DLog

Swift: 5.9, 5.8, 5.7, 5.6 Platforms: iOS, macOS, tvOS, visionOS, watchOS Swift Package Manager: compatible Build: status Codecov: code coverage

Donate

DLog: Modern logger with pipelines for Swift DLog: Xcode Console

DLog 是 Swift 的开发日志记录器,支持表情符号和彩色文本输出、格式和隐私选项、管道、过滤、作用域、间隔、堆栈回溯等功能。

入门

默认情况下,DLog 提供基本的文本控制台输出

// Import DLog package
import DLog

// Create the logger
let logger = DLog()

// Log a message
logger.log("Hello DLog!")

输出

• 23:59:11.710 [DLOG] [LOG] <DLog.swift:12> Hello DLog!

在哪里

您可以将隐私和格式选项应用于记录的值

let cardNumber = "1234 5678 9012 3456"
logger.debug("\(cardNumber, privacy: .private(mask: .redact))")

let salary = 10_123
logger.debug("\(salary, format: .number(style: .currency))")

输出

• 12:20:29.462 [DLOG] [DEBUG] <DLogTests.swift:539> 0000 0000 0000 0000
• 12:20:29.464 [DLOG] [DEBUG] <DLogTests.swift:542> $10,123.00

默认情况下,DLog 将文本日志输出到 stdout,但您可以使用其他输出,例如:stderr、过滤器、文件、OSLog、Net。例如

let logger = DLog(.file("path/dlog.txt"))
logger.debug("It's a file log!")

Dlog 支持纯文本(默认)、表情符号和彩色样式用于文本消息,您可以设置需要的样式

let logger = DLog(.textEmoji => .stdout)

logger.info("Info message")
logger.log("Log message")
logger.assert(false, "Assert message")

输出

• 00:03:07.179 [DLOG] ✅ [INFO] <DLog.swift:6> Info message
• 00:03:07.181 [DLOG] 💬 [LOG] <DLog.swift:7> Log message
• 00:03:07.181 [DLOG] 🅰️ [ASSERT] <DLog.swift:8> Assert message

其中 => 是管道运算符,可用于创建输出列表

let logger = DLog(.textEmoji
    => .stdout
    => .filter { $0.type == .error }
    => .file("path/error.log"))

所有日志消息将首先写入 stdout,然后仅将错误消息写入文件。

日志级别

log

记录消息

logger.log("App start")

输出

• 23:40:23.545 [DLOG] [LOG] <DLog.swift:12> App start

info

记录信息消息和有用的数据

let uuid = UUID().uuidString
logger.info("uuid: \(uuid)")

输出

• 23:44:30.702 [DLOG] [INFO] <DLog.swift:13> uuid: 8A71D2B9-29F1-4330-A4C2-69988E3FE172

trace

记录当前函数名称和消息(如果提供),以帮助调试开发期间的问题

let logger = DLog()

func startup() {
    logger.trace()
    logger.trace("start")
}
startup()

输出

• 10:09:08.343 [DLOG] [TRACE] <DLogTests.swift:174> {func:startup,thread:{name:main,number:1}}
• 10:09:08.345 [DLOG] [TRACE] <DLogTests.swift:175> {func:startup,thread:{name:main,number:1}} start

debug

记录调试消息,以帮助调试开发期间的问题

let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: "https://apple.com")!) { data, response, error in
    guard let http = response as? HTTPURLResponse else { return }

    let text = HTTPURLResponse.localizedString(forStatusCode: http.statusCode)
    logger.debug("\(http.url!.absoluteString): \(http.statusCode) - \(text)")
}
.resume()

输出

• 23:49:16.562 [DLOG] [DEBUG] <DLog.swift:17> https://www.apple.com/: 200 - no error

warning

记录代码执行期间发生的警告消息。

logger.warning("No Internet connection.")

输出

• 23:49:55.757 [DLOG] [WARNING] <DLog.swift:12> No Internet connection.

error

记录代码执行期间发生的错误。

let fromURL = URL(fileURLWithPath: "source.txt")
let toURL = URL(fileURLWithPath: "destination.txt")
do {
    try FileManager.default.moveItem(at: fromURL, to: toURL)
}
catch {
    logger.error("\(error.localizedDescription)")
}

输出

• 23:50:39.560 [DLOG] [ERROR] <DLog.swift:18> “source.txt” couldn’t be moved to “Macintosh HD” because either the former doesn’t exist, or the folder containing the latter doesn’t exist.

assert

完整性检查并在条件为 false 时记录消息(如果提供)。

let user = "John"
let password = ""

logger.assert(user.isEmpty == false, "User is empty")
logger.assert(password.isEmpty == false)
logger.assert(password.isEmpty == false, "Password is empty")

输出

• 23:54:19.420 [DLOG] [ASSERT] <DLog.swift:16>
• 23:54:19.422 [DLOG] [ASSERT] <DLog.swift:17> Password is empty

fault

记录代码执行期间发生的严重错误。

guard let modelURL = Bundle.main.url(forResource: "DataModel", withExtension:"momd") else {
    logger.fault("Error loading model from bundle")
    abort()
}

输出

• 23:55:07.445 [DLOG] [FAULT] <DLog.swift:13> Error loading model from bundle

隐私

隐私选项允许管理日志消息中值的可见性。

public

默认情况下,它适用于日志消息中的所有值,并且这些值将在日志中可见。

let phoneNumber = "+11234567890"
logger.log("\(phoneNumber)") // public by default
logger.log("\(phoneNumber, privacy: .public)")

输出

• 20:16:18.628 [DLOG] [LOG] <DLogTests.swift:481> +11234567890
• 20:16:18.629 [DLOG] [LOG] <DLogTests.swift:482> +11234567890

private

由于用户可以访问您的应用程序生成的日志消息,请使用 private 隐私选项来隐藏潜在的敏感信息。 例如,您可以使用它来隐藏或屏蔽帐户信息或个人数据。

标准的 private 选项使用通用字符串编辑值。

let phoneNumber = "+11234567890"
logger.log("\(phoneNumber, privacy: .private)")

输出

• 12:04:29.758 [DLOG] [LOG] <DLogTests.swift:508> <private>

private(mask: .hash)

mask 选项使用其哈希值编辑日志中的值。

logger.log("\(phoneNumber, privacy: .private(mask: .hash))")

输出

• 12:09:14.892 [DLOG] [LOG] <DLogTests.swift:508> ECD0ACC2

private(mask: .random)

mask 选项使用日志中每个符号的随机值来编辑值。

logger.log("\(phoneNumber, privacy: .private(mask: .random))")

输出

• 12:16:19.109 [DLOG] [LOG] <DLogTests.swift:508> =15277829896

private(mask: .redact)

mask 选项使用日志中每个符号的通用值来编辑值。

logger.log("\(phoneNumber, privacy: .private(mask: .redact))")

输出

• 12:20:02.217 [DLOG] [LOG] <DLogTests.swift:508> +00000000000

private(mask: .shuffle)

mask 选项使用日志中所有符号的随机排列值编辑值。

logger.log("\(phoneNumber, privacy: .private(mask: .shuffle))")

输出

• 12:23:01.864 [DLOG] [LOG] <DLogTests.swift:508> 47681901+352

private(mask: .custom(value:))

mask 选项使用日志中的自定义字符串值编辑值。

logger.log("\(phoneNumber, privacy: .private(mask: .custom(value: "<phone>")))")

输出

• 12:28:55.105 [DLOG] [LOG] <DLogTests.swift:508> <phone>

private(mask: .reduce(length:))

mask 选项使用日志中提供的长度的缩减值编辑值。

logger.log("\(phoneNumber, privacy: .private(mask: .reduce(length: 5)))")

输出

• 12:30:48.076 [DLOG] [LOG] <DLogTests.swift:508> +1...890

private(mask: .partial(first:, last:))

mask 选项使用日志中提供的长度的开头和结尾部分来编辑值。

logger.log("\(phoneNumber, privacy: .private(mask: .partial(first: 2, last: 1)))")

输出

• 12:36:58.950 [DLOG] [LOG] <DLogTests.swift:508> +1*********0

格式化器

DLog 根据默认设置格式化日志消息中的值,但您可以将自定义格式应用于变量,以使其更具可读性。

Date

日期值的格式化选项。

date(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, locale: Locale?)

Date 值的格式化选项。

let date = Date()
logger.log("\(date, format: .date(dateStyle: .medium))")
logger.log("\(date, format: .date(timeStyle: .short))")
logger.log("\(date, format: .date(dateStyle: .medium, timeStyle: .short))")
logger.log("\(date, format: .date(dateStyle: .medium, timeStyle: .short, locale: Locale(identifier: "en_GB")))")

输出

• 18:12:52.604 [DLOG] [LOG] <DLogTests.swift:555> Mar 30, 2022
• 18:12:52.605 [DLOG] [LOG] <DLogTests.swift:556> 6:12 PM
• 18:12:52.606 [DLOG] [LOG] <DLogTests.swift:557> Mar 30, 2022 at 6:12 PM
• 18:12:52.606 [DLOG] [LOG] <DLogTests.swift:559> 30 Mar 2022 at 18:12

dateCustom(format: String)

使用自定义格式字符串格式化日期。

let date = Date()
logger.log("\(date, format: .dateCustom(format: "dd-MM-yyyy"))")

输出

• 18:12:52.606 [DLOG] [LOG] <DLogTests.swift:558> 30-03-2022

Integer

整数(Int8、Int16、Int32、Int64、UInt8 等)值的格式化选项。

binary

以二进制格式显示整数值。

let value = 12345
logger.log("\(value, format: .binary)")

输出

• 18:58:29.085 [DLOG] [LOG] <DLogTests.swift:621> 11000000111001

octal(includePrefix: Bool)

使用指定参数以八进制格式显示整数值。

let value = 12345
logger.log("\(value, format: .octal)")
logger.log("\(value, format: .octal(includePrefix: true))")

输出

• 19:01:33.019 [DLOG] [LOG] <DLogTests.swift:624> 30071
• 19:01:33.020 [DLOG] [LOG] <DLogTests.swift:625> 0o30071

hex(includePrefix: Bool, uppercase: Bool)

使用指定参数以十六进制格式显示整数值。

let value = 1234567
logger.log("\(value, format: .hex)")
logger.log("\(value, format: .hex(includePrefix: true))")
logger.log("\(value, format: .hex(uppercase: true))")
logger.log("\(value, format: .hex(includePrefix: true, uppercase: true))")

输出

• 19:06:30.463 [DLOG] [LOG] <DLogTests.swift:621> 12d687
• 19:06:30.464 [DLOG] [LOG] <DLogTests.swift:622> 0x12d687
• 19:06:30.464 [DLOG] [LOG] <DLogTests.swift:623> 12D687
• 19:06:30.464 [DLOG] [LOG] <DLogTests.swift:624> 0x12D687

byteCount(countStyle: ByteCountFormatter.CountStyle, allowedUnits: ByteCountFormatter.Units)

使用样式和单位格式化字节数。

let value = 20_234_557
logger.log("\(value, format: .byteCount)")
logger.log("\(value, format: .byteCount(countStyle: .memory))")
logger.log("\(value, format: .byteCount(allowedUnits: .useBytes))")
logger.log("\(value, format: .byteCount(countStyle: .memory, allowedUnits: .useGB))")

输出

• 19:36:49.454 [DLOG] [LOG] <DLogTests.swift:621> 20.2 MB
• 19:36:49.458 [DLOG] [LOG] <DLogTests.swift:622> 19.3 MB
• 19:36:49.458 [DLOG] [LOG] <DLogTests.swift:623> 20,234,557 bytes
• 19:36:49.458 [DLOG] [LOG] <DLogTests.swift:624> 0.02 GB

number(style: NumberFormatter.Style, locale: Locale?)

使用指定参数以数字格式显示整数值。

let number = 1_234
logger.log("\(number, format: .number)")
logger.log("\(number, format: .number(style: .currency))")
logger.log("\(number, format: .number(style: .spellOut))")
logger.log("\(number, format: .number(style: .currency, locale: Locale(identifier: "en_GB")))")

输出

• 19:42:40.938 [DLOG] [LOG] <DLogTests.swift:621> 1,234
• 19:42:40.939 [DLOG] [LOG] <DLogTests.swift:622> $1,234.00
• 19:42:40.939 [DLOG] [LOG] <DLogTests.swift:623> one thousand two hundred thirty-four
• 19:42:40.939 [DLOG] [LOG] <DLogTests.swift:624> £1,234.00

httpStatusCode

显示与指定 HTTP 状态代码对应的本地化字符串。

logger.log("\(200, format: .httpStatusCode)")
logger.log("\(404, format: .httpStatusCode)")
logger.log("\(500, format: .httpStatusCode)")

输出

• 19:51:14.297 [DLOG] [LOG] <DLogTests.swift:620> HTTP 200 no error
• 19:51:14.298 [DLOG] [LOG] <DLogTests.swift:621> HTTP 404 not found
• 19:51:14.298 [DLOG] [LOG] <DLogTests.swift:622> HTTP 500 internal server error

ipv4Address

将整数值 (Int32) 显示为 IPv4 地址。

let ip4 = 0x0100007f
logger.log("\(ip4, format: .ipv4Address)")

输出

• 19:53:18.141 [DLOG] [LOG] <DLogTests.swift:621> 127.0.0.1

time(unitsStyle: DateComponentsFormatter.UnitsStyle)

显示秒的时间长度。

let time = 60 * 60 + 23 * 60 + 15 // 1h 23m 15s
logger.log("\(time, format: .time)")
logger.log("\(time, format: .time(unitsStyle: .positional))")
logger.log("\(time, format: .time(unitsStyle: .short))")

输出

• 12:56:39.655 [DLOG] [LOG] <DLogTests.swift:624> 1h 23m 15s
• 12:56:39.657 [DLOG] [LOG] <DLogTests.swift:625> 1:23:15
• 12:56:39.657 [DLOG] [LOG] <DLogTests.swift:626> 1 hr, 23 min, 15 secs

date(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, locale: Locale?)

显示自 1970 年以来的秒数的日期。

let timeIntervalSince1970 = 1645026131 // 2022-02-16 15:42:11 +0000
logger.log("\(timeIntervalSince1970, format: .date)")
logger.log("\(timeIntervalSince1970, format: .date(dateStyle: .short))")
logger.log("\(timeIntervalSince1970, format: .date(timeStyle: .medium))")

输出

• 13:00:33.964 [DLOG] [LOG] <DLogTests.swift:624> 2/16/22, 3:42 PM
• 13:00:33.965 [DLOG] [LOG] <DLogTests.swift:625> 2/16/22
• 13:00:33.966 [DLOG] [LOG] <DLogTests.swift:626> 3:42:11 PM

Float

双精度和浮点数的格式化选项。

fixed(precision: Int)

以 fprintf 的 %f 格式和指定精度显示浮点值。

let value = 12.345
logger.log("\(value, format: .fixed)")
logger.log("\(value, format: .fixed(precision: 2))")

输出

• 08:49:35.800 [DLOG] [LOG] <DLogTests.swift:696> 12.345000
• 08:49:35.802 [DLOG] [LOG] <DLogTests.swift:697> 12.35

hex(includePrefix: Bool, uppercase: Bool)

以十六进制格式和指定参数显示浮点值。

let value = 12.345
logger.log("\(value, format: .hex)")
logger.log("\(value, format: .hex(includePrefix: true))")
logger.log("\(value, format: .hex(uppercase: true))")
logger.log("\(value, format: .hex(includePrefix: true, uppercase: true))")

输出

• 09:25:46.834 [DLOG] [LOG] <DLogTests.swift:697> 1.8b0a3d70a3d71p+3
• 09:25:46.836 [DLOG] [LOG] <DLogTests.swift:698> 0x1.8b0a3d70a3d71p+3
• 09:25:46.836 [DLOG] [LOG] <DLogTests.swift:699> 1.8B0A3D70A3D71P+3
• 09:25:46.836 [DLOG] [LOG] <DLogTests.swift:700> 0x1.8B0A3D70A3D71P+3

exponential(precision: Int)

以 fprintf 的 %e 格式和指定精度显示浮点值。

let value = 12.345
logger.log("\(value, format: .exponential)")
logger.log("\(value, format: .exponential(precision: 2))")

输出

• 09:28:51.684 [DLOG] [LOG] <DLogTests.swift:696> 1.234500e+01
• 09:28:51.686 [DLOG] [LOG] <DLogTests.swift:697> 1.23e+01

hybrid(precision: Int)

以 fprintf 的 %g 格式和指定精度显示浮点值。

let value = 12.345
logger.log("\(value, format: .hybrid)")
logger.log("\(value, format: .hybrid(precision: 1))")

输出

• 09:31:02.301 [DLOG] [LOG] <DLogTests.swift:696> 12.345
• 09:31:02.303 [DLOG] [LOG] <DLogTests.swift:697> 1e+01

number(style: NumberFormatter.Style, locale: Locale?)

以数字格式和指定参数显示浮点值。

let value = 12.345
logger.log("\(value, format: .number)")
logger.log("\(value, format: .number(style: .currency))")
logger.log("\(value, format: .number(style: .spellOut))")
logger.log("\(value, format: .number(style: .currency, locale: Locale(identifier: "en_GB")))")

输出

• 09:35:00.740 [DLOG] [LOG] <DLogTests.swift:696> 12.345
• 09:35:00.741 [DLOG] [LOG] <DLogTests.swift:697> $12.34
• 09:35:00.741 [DLOG] [LOG] <DLogTests.swift:698> twelve point three four five
• 09:35:00.744 [DLOG] [LOG] <DLogTests.swift:699> £12.34

time(unitsStyle: DateComponentsFormatter.UnitsStyle)

显示秒的时间长度。

let time = 60 * 60 + 23 * 60 + 1.25 // 1m 23m 1.25s
logger.log("\(time, format: .time)")
logger.log("\(time, format: .time(unitsStyle: .positional))")
logger.log("\(time, format: .time(unitsStyle: .short))")

输出

• 13:06:29.874 [DLOG] [LOG] <DLogTests.swift:714> 1h 23m 1.250s
• 13:06:29.877 [DLOG] [LOG] <DLogTests.swift:715> 1:23:01.250
• 13:06:29.878 [DLOG] [LOG] <DLogTests.swift:716> 1 hr, 23 min, 1.250 sec

date(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style, locale: Locale?)

显示自 1970 年以来的秒数的日期。

let timeIntervalSince1970 = 1645026131.45 // 2022-02-16 15:42:11 +0000
logger.log("\(timeIntervalSince1970, format: .date)")
logger.log("\(timeIntervalSince1970, format: .date(dateStyle: .short))")
logger.log("\(timeIntervalSince1970, format: .date(timeStyle: .medium))")

输出

• 13:09:51.299 [DLOG] [LOG] <DLogTests.swift:714> 2/16/22, 3:42 PM
• 13:09:51.300 [DLOG] [LOG] <DLogTests.swift:715> 2/16/22
• 13:09:51.301 [DLOG] [LOG] <DLogTests.swift:716> 3:42:11 PM

Bool

布尔值的格式化选项。

binary

将布尔值显示为 1 或 0。

let value = true
logger.log("\(value, format: .binary)")
logger.log("\(!value, format: .binary)")

输出

• 09:41:38.368 [DLOG] [LOG] <DLogTests.swift:746> 1
• 09:41:38.370 [DLOG] [LOG] <DLogTests.swift:747> 0

answer

将布尔值显示为 yes 或 no。

let value = true
logger.log("\(value, format: .answer)")
logger.log("\(!value, format: .answer)")

输出

• 09:42:57.414 [DLOG] [LOG] <DLogTests.swift:746> yes
• 09:42:57.415 [DLOG] [LOG] <DLogTests.swift:747> no

toggle

将布尔值显示为 on 或 off。

let value = true
logger.log("\(value, format: .toggle)")
logger.log("\(!value, format: .toggle)")

输出

• 09:43:46.202 [DLOG] [LOG] <DLogTests.swift:746> on
• 09:43:46.203 [DLOG] [LOG] <DLogTests.swift:747> off

Data

Data 的格式化选项。

ipv6Address

从数据中漂亮地打印 IPv6 地址。

let data = Data([0x20, 0x01, 0x0b, 0x28, 0xf2, 0x3f, 0xf0, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a])
logger.log("\(data, format: .ipv6Address)")

输出

• 13:24:50.534 [DLOG] [LOG] <DLogTests.swift:813> 2001:b28:f23f:f005::a

text

从数据中漂亮地打印文本。

let data = Data([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x44, 0x4c, 0x6f, 0x67, 0x21])
logger.log("\(data, format: .text)")

输出

• 13:31:37.596 [DLOG] [LOG] <DLogTests.swift:813> Hello DLog!

uuid

从数据中漂亮地打印 uuid。

let data = Data([0xca, 0xcd, 0x1b, 0x9d, 0x56, 0xaa, 0x41, 0xf0, 0xbd, 0xe3, 0x45, 0x7d, 0xda, 0x30, 0xa8, 0xd4])
logger.log("\(data, format: .uuid)")

输出

• 14:41:26.818 [DLOG] [LOG] <DLogTests.swift:815> CACD1B9D-56AA-41F0-BDE3-457DDA30A8D4

raw

从数据中漂亮地打印原始字节。

let data = Data([0xab, 0xcd, 0xef])
logger.log("\(data, format: .raw)")

输出

• 14:42:52.795 [DLOG] [LOG] <DLogTests.swift:815> ABCDEF

作用域(Scope)

scope 提供了一种机制来对程序中完成的工作进行分组,以便在树状视图中查看与代码定义的范围相关的所有日志消息

logger.scope("Loading") { scope in
    if let path = Bundle.main.path(forResource: "data", ofType: "json") {
        scope.info("File: \(path)")
        if let data = try? String(contentsOfFile: path) {
            scope.debug("Loaded \(data.count) bytes")
        }
    }
}

注意:要将消息固定到作用域,您应该使用提供的作用域实例来调用 logtrace 等。

输出

• 23:57:13.410 [DLOG] ┌ [Loading]
• 23:57:13.427 [DLOG] ├ [INFO] <DLog.swift:14> File: path/data.json
• 23:57:13.443 [DLOG] ├ [DEBUG] <DLog.swift:16> Loaded 121 bytes
• 23:57:13.443 [DLOG] └ [Loading] (0.330s)

在哪里

您可以以编程方式获取已完成作用域的持续时间值

var scope = logger.scope("scope") { _ in
    ...
}

print(scope.duration) // Time duration

可以异步地 enterleave 作用域

let scope = logger.scope("Request")
scope.enter()

let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: "https://apple.com")!) { data, response, error in
    defer {
        scope.leave()
    }

    guard let data = data, let http = response as? HTTPURLResponse else {
        return
    }

    scope.debug("\(http.url!.absoluteString) - HTTP \(http.statusCode)")
    scope.debug("Loaded: \(data.count) bytes")
}
.resume()

输出

• 00:01:24.158 [DLOG] ┌ [Request]
• 00:01:24.829 [DLOG] ├ [DEBUG] <DLog.swift:25> https://www.apple.com/ - HTTP 200
• 00:01:24.830 [DLOG] ├ [DEBUG] <DLog.swift:26> Loaded: 74454 bytes
• 00:01:24.830 [DLOG] └ [Request] (0.671s)

作用域可以嵌套在一起,这实现了一个全局作用域堆栈

logger.scope("Loading") { scope1 in
    if let url = Bundle.main.url(forResource: "data", withExtension: "json") {
        scope1.info("File: \(url)")

        if let data = try? Data(contentsOf: url) {
            scope1.debug("Loaded \(data.count) bytes")

            logger.scope("Parsing") { scope2 in
                if let items = try? JSONDecoder().decode([Item].self, from: data) {
                    scope2.debug("Parsed \(items.count) items")
                }
            }
        }
    }
}

输出

• 00:03:13.552 [DLOG] ┌ [Loading]
• 00:03:13.554 [DLOG] ├ [INFO] <DLog.swift:20> File: file:///path/data.json
• 00:03:13.555 [DLOG] ├ [DEBUG] <DLog.swift:23> Loaded 121 bytes
• 00:03:13.555 [DLOG] | ┌ [Parsing]
• 00:03:13.557 [DLOG] | ├ [DEBUG] <DLog.swift:27> Parsed 3 items
• 00:03:13.557 [DLOG] | └ [Parsing] (0.200s)
• 00:03:13.609 [DLOG] └ [Loading] (0.560s)

间隔(Interval)

interval 通过运行时间来衡量代码的性能,并记录一条详细消息,其中包含以秒为单位累积的统计信息

for _ in 0..<10 {
    logger.interval("sorting") {
        var arr = (1...10000).map {_ in arc4random()}
        arr.sort()
    }
}

输出

• 20:00:01.439 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.022s,duration:0.022s} sorting
• 20:00:01.462 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.022s,duration:0.022s} sorting
• 20:00:01.484 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.022s,duration:0.022s} sorting
• 20:00:01.507 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.022s,duration:0.022s} sorting
• 20:00:01.528 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.022s,duration:0.022s} sorting
• 20:00:01.550 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.022s,duration:0.021s} sorting
• 20:00:01.570 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.021s,duration:0.020s} sorting
• 20:00:01.591 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.021s,duration:0.020s} sorting
• 20:00:01.611 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.021s,duration:0.020s} sorting
• 20:00:01.632 [DLOG] [INTERVAL] <DLogTests.swift:518> {average:0.021s,duration:0.020s} sorting

在哪里

您还可以以编程方式获取当前间隔的持续时间和所有统计信息

let interval = logger.interval("signpost") {
    ...
}

print(interval.duration) // The current duration

print(interval.statistics.count) // Total count of calls
print(interval.statistics.total) // Total time of all durations
print(interval.statistics.min) // Min duration
print(interval.statistics.max) // Max duration
print(interval.statistics.average) // Average duraton

要衡量异步任务,您可以使用 beginend 方法

let logger = DLog()

let interval = logger.interval("load")
interval.begin()

let url = URL(string: "https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!
let asset = AVURLAsset(url: url)
asset.loadValuesAsynchronously(forKeys: ["duration"]) {
    let status = asset.statusOfValue(forKey: "duration", error: nil)
    if status == .loaded {
        logger.debug("duration: \(asset.duration.seconds)")
    }
    interval.end()
}

输出

• 18:56:38.134 [DLOG] [DEBUG] <DLogTests.swift:528> duration: 210.0
• 18:56:38.135 [DLOG] [INTERVAL] <DLogTests.swift:520> {average:7.979s,duration:7.979s} load

类别(Category)

您可以定义类别名称来区分应用程序的唯一区域和部分,DLog 使用此值来分类和过滤相关的日志消息。 例如,您可以为应用程序的用户界面、数据模型和网络代码定义单独的字符串。

let logger = DLog()
let tableLogger = logger["TABLE"]
let netLogger = logger["NET"]

logger.debug("Refresh")
netLogger.debug("Successfully fetched recordings.")
tableLogger.debug("Updating with network response.")

输出

• 00:11:30.660 [DLOG] [DEBUG] <DLog.swift:22> Refresh
• 00:11:30.661 [NET] [DEBUG] <DLog.swift:23> Successfully fetched recordings.
• 00:11:30.661 [TABLE] [DEBUG] <DLog.swift:24> Updating with network response.

配置

您可以将特定配置应用于类别,以更改默认日志消息的外观、可见信息和详细信息等。(更多信息请参见:配置

例如

let logger = DLog()

var config = LogConfig()
config.sign = ">"
config.options = [.sign, .time, .category, .type]
config.traceConfig.options = [.queue]

let netLogger = logger.category(name: "NET", config: config)

logger.trace("default")
netLogger.trace("net")

输出

• 10:12:12.332 [DLOG] [TRACE] <DLogTests.swift:182> {func:test,thread:{name:main,number:1}} default
> 10:12:12.334 [NET] [TRACE] net

元数据(Metadata)

在其最基本的使用中,元数据对于将关于同一主题的日志消息分组在一起非常有用。 例如,您可以将 HTTP 请求的请求 ID 设置为元数据,并且所有关于该 HTTP 请求的日志行都会显示该请求 ID。

Logger 元数据是一个存储在字典中的关键字列表,可以在创建时应用于 logger,或者稍后使用其实例进行更改,例如

let logger = DLog(metadata: ["id" : 12345])
logger.log("start")

logger.metadata["process"] = "main"
logger.log("attach")

logger.metadata.clear() // Clear metadata
logger.log("finish")

输出

• 10:13:11.372 [DLOG] [LOG] <DLogTests.swift:174> {id:12345} start
• 10:13:11.374 [DLOG] [LOG] <DLogTests.swift:177> {id:12345,process:main} attach
• 10:13:11.374 [DLOG] [LOG] <DLogTests.swift:180> finish

其中:(id:12345,process:main) - 当前元数据的键值对。

类别和作用域的工作方式相同,它们在创建时复制其父元数据,但此副本可以在以后更改

let logger = DLog(metadata: ["id" : 12345])
logger.log("start")
        
// Scope
logger.scope("scope") { scope in
    scope.log("start")
    scope.metadata["id"] = nil // Remove "id" kev-value pair
    scope.log("finish")
}

// Category
let category = logger["NET"]
category.metadata["method"] = "POST"
category.log("post data")
category.log("receive response")

category.metadata.clear()
category.log("close")

输出

• 10:13:54.542 [DLOG] [LOG] <DLogTests.swift:174> {id:12345} start
• 10:13:54.543 [DLOG] ┌ [scope] 
• 10:13:54.543 [DLOG] ├ [LOG] <DLogTests.swift:178> {id:12345} start
• 10:13:54.544 [DLOG] ├ [LOG] <DLogTests.swift:180> finish
• 10:13:54.544 [DLOG] └ [scope] (0s)
• 10:13:54.546 [NET] [LOG] <DLogTests.swift:186> {id:12345,method:POST} post data
• 10:13:54.546 [NET] [LOG] <DLogTests.swift:187> {id:12345,method:POST} receive response
• 10:13:54.546 [NET] [LOG] <DLogTests.swift:190> close

输出

Text

Text 是一个源输出,它生成日志消息的文本表示形式。 它不会将文本传递到任何目标输出(stdout、文件等),通常其他输出会使用它。

它支持三种样式

let outputs = [
    "Plain" : Text(style: .plain),
    "Emoji" : Text(style: .emoji),
    "Colored" : Text(style: .colored),
]

for (name, output) in outputs {
    let logger = DLog(output)

    print(name)
    print(logger.info("info")!)
    print(logger.error("error")!)
    print(logger.fault("fatal")!)
    print("")
}

输出

Plain
• 00:12:31.718 [DLOG] [INFO] <DLog.swift:25> info
• 00:12:31.719 [DLOG] [ERROR] <DLog.swift:26> error
• 00:12:31.720 [DLOG] [FAULT] <DLog.swift:27> fatal

Emoji
• 00:12:31.720 [DLOG] ✅ [INFO] <DLog.swift:25> info
• 00:12:31.721 [DLOG] ⚠️ [ERROR] <DLog.swift:26> error
• 00:12:31.734 [DLOG] 🆘 [FAULT] <DLog.swift:27> fatal

Colored
�[2m•�[0m �[2m00:12:31.735�[0m �[34mDLOG�[0m �[42m�[37m INFO �[0m �[2m�[32m<DLog.swift:25>�[0m �[32minfo�[0m
�[2m•�[0m �[2m00:12:31.735�[0m �[34mDLOG�[0m �[43m�[30m ERROR �[0m �[2m�[33m<DLog.swift:26>�[0m �[33merror�[0m
�[2m•�[0m �[2m00:12:31.735�[0m �[34mDLOG�[0m �[41m�[37m�[5m FAULT �[0m �[2m�[31m<DLog.swift:27>�[0m �[31mfatal�[0m

终端中的彩色文本

DLog.swift: Colored text log in Terminal

您还可以使用快捷方式 .textPlain.textEmoji.textColored 来创建输出

let logger = DLog(.textEmoji)

Standard

Standard 是一个目标输出,可以将文本消息输出到 POSIX 流。

// Prints to stdout
let loggerOut = DLog(Standard())

// Prints to stderr
let loggerErr = DLog(Standard(stream: Darwin.stderr))

您还可以使用快捷方式 .stdout.stderr 为记录器创建输出。

let logger = DLog(.stderr)
logger.info("It's error stream")

默认情况下,Standard 使用 Text(style: .plain) 输出作为将文本写入流的源,但您可以设置其他输出。

let output = Standard(source: .textEmoji)
let logger = DLog(output)

logger.info("Emoji")

输出

• 00:15:25.602 [DLOG] ✅ [INFO] <DLog.swift:18> Emoji

文件

File 是一个目标输出,它将文本消息写入由提供的路径指定的文件。

let file = File(path: "/users/user/dlog.txt")
let logger = DLog(file)

logger.info("It's a file")

默认情况下,File 输出会清除已打开文件的内容,但如果您想将数据附加到现有文件,则应将 append 参数设置为 true

let file = File(path: "/users/user/dlog.txt", append: true)

您还可以使用 .file 快捷方式创建输出。

let logger = DLog(.file("dlog.txt"))

默认情况下,File 输出使用 Text(style: .plain) 作为源,但您可以更改它。

let file = File(path: "/users/user/dlog.txt", source: .textColored)
let logger = DLog(file)

logger.scope("File") { scope in
    scope.info("It's a file")
}

文件 "dlog.txt"

DLog: Colored text log in a file.

OSLog

OSLog 是一个目标输出,它将消息写入统一日志系统 (https://developer.apple.com/documentation/os/logging),该系统捕获应用程序中的遥测数据,以进行调试和性能分析。然后,您可以使用各种工具来检索日志信息,例如:ConsoleInstruments 应用程序,命令行工具 log 等。

要创建 OSLog,您可以使用子系统字符串来标识应用程序的主要功能区域,并在反向 DNS 表示法中指定它们,例如,com.your_company.your_subsystem_name。默认情况下,OSLog 使用 com.dlog.logger 子系统。

let output1 = OSLog() // subsystem = "com.dlog.logger"
let output2 = OSLog(subsystem: "com.company.app") // subsystem = "com.company.app"

您还可以使用 .oslog 快捷方式创建输出。

let logger1 = DLog(.oslog)
let logger2 = DLog(.oslog("com.company.app"))

所有 DLog 的方法都映射到系统记录器的方法,并具有适当的日志级别,例如。

let logger = DLog(.oslog)

logger.log("log")
logger.info("info")
logger.trace("trace")
logger.debug("debug")
logger.warning("warning")
logger.error("error")
logger.assert(false, "assert")
logger.fault("fault")

带有日志级别的 Console.app

DLog: Logs in Console.app

DLog 的作用域映射到系统记录器的活动。

let logger = DLog(.oslog)

logger.scope("Loading") { scope1 in
    scope1.info("start")
    logger.scope("Parsing") { scope2 in
        scope2.debug("Parsed 1000 items")
    }
    scope1.info("finish")
}

带有活动的 Console.app

DLog: Activities in Console.app

DLog 的间隔映射到系统记录器的标记。

let logger = DLog(.oslog)

for _ in 0..<10 {
    logger.interval("Sorting") {
        let delay = [0.1, 0.2, 0.3].randomElement()!
        Thread.sleep(forTimeInterval: delay)
        logger.debug("Sorted")
    }
}

带有标记的 Instruments.app

DLog: Signposts in Instruments.app

Net

Net 是一个目标输出,它将日志消息发送到 NetConsole 服务,该服务可以从您机器上的命令行运行。 该服务作为 DLog 包中的可执行文件提供,要启动它,您应该运行 sh NetConsole.command(或者直接点击 NetConsole.command 文件)在包的文件夹中,然后该服务开始侦听传入的消息。

$ sh NetConsole.command # or 'xcrun --sdk macosx swift run'
> [39/39] Linking NetConsole
> NetConsole for DLog v.1.0

然后,该输出连接并将您的日志消息发送到 NetConsole

let logger = DLog(Net())

logger.scope("Main") { scope1 in
    scope1.trace("Start")
    logger.scope("Subtask") { scope2 in
        scope2.info("Validation")
        scope2.error("Token is invalid")
        scope2.debug("Retry")
    }
    scope1.info("Close connection")
}

iOS 14: 不要忘记在您的 Info.plist 中进行以下更改以支持 Bonjour。

<key>NSLocalNetworkUsageDescription</key>
<string>Looking for local tcp Bonjour  service</string>
<key>NSBonjourServices</key>
<array>
    <string>_dlog._tcp</string>
</array>

终端

DLog: Colored text log in NetConsole

默认情况下,Net 使用 Text(style: .colored) 输出作为源,但您可以设置其他输出。

let logger = DLog(Net(source: .textEmoji))

您还可以使用 .net 快捷方式为记录器创建输出。

let logger = DLog(.net)

要连接到网络中服务的特定实例,您应该为 NetConsoleNet 输出提供一个唯一的名称(默认情况下使用 "DLog" 名称)。

要使用特定名称运行 NetConsole,请运行以下命令

sh NetConsole.command -n "MyLogger" # or 'xcrun --sdk macosx swift run NetConsole -n "MyLogger"'

在 swift 代码中,您应该设置相同的名称

let logger = DLog(.net("MyLogger"))

有关 NetConsole 的更多参数,您可以查看帮助。

sh NetConsole.command --help  # or 'xcrun --sdk macosx swift run NetConsole --help'
OVERVIEW: NetConsole for DLog v.1.0

USAGE: net-console [--name <name>] [--auto-clear] [--debug]

OPTIONS:
  -n, --name <name>       The name by which the service is identified to the network. The name must be unique and by default it equals
                          "DLog". If you pass the empty string (""), the system automatically advertises your service using the computer
                          name as the service name.
  -a, --auto-clear        Clear a terminal on new connection.
  -d, --debug             Enable debug messages.
  -h, --help              Show help information.

管道(Pipeline)

如上所述,FileNetStandard 输出在其初始化程序中都有 source 参数,用于设置默认源输出,这非常有用,如果我们想要更改默认输出。

let std = Standard(stream: .out, source: .textEmoji)
let logger = DLog(std)

实际上,任何输出都有 source 属性。

let std = Standard()
std.source = .textEmoji
let logger = DLog(std)

因此,可以创建一个输出的链表。

// Text
let text: LogOutput = .textEmoji

// Standard
let std = Standard()
std.source = text

// File
let file = File(path: "dlog.txt")
file.source = std

let logger = DLog(file)

其中 textstd 的源,而 stdfile 的源:text --> std --> file,现在每条文本消息将连续发送到 stdfile 输出。

让我们用更简洁的方式重写这个例子。

let logger = DLog(.textEmoji => .stdout => .file("dlog.txt"))

其中 => 是管道运算符,它定义了来自两个输出的组合输出,其中第一个是源,第二个是目标。 所以从上面的例子中,emoji 文本消息将被写入两次:首先写入标准输出,然后写入文件。

您可以将任何需要的输出组合在一起,并从多个输出创建最终的链式输出,您的消息将被依次转发到所有这些输出。

// All log messages will be written:
// 1) as plain text to stdout
// 2) as colored text (with escape codes) to the file

let logger = DLog(.textPlain => .stdout => .textColored => .file(path))

过滤器(Filter)

Filter.filter 表示一个管道输出,可以通过以下可用字段过滤日志消息:timecategorytypefileNamefuncNamelinetextscope。您可以将其注入到您需要仅记录特定数据的管道中。

例子

  1. 仅将带有 'NET' 类别的日志消息记录到标准输出
let logger = DLog(.textPlain => .filter { $0.category == "NET" } => .stdout)
let netLogger = logger["NET"]

logger.info("info")
netLogger.info("info")

输出

• 00:17:58.076 [NET] [INFO] <DLog.swift:19> info
  1. 仅记录调试消息
let logger = DLog(.textPlain => .filter { $0.type == .debug } => .stdout)

logger.trace()
logger.info("info")
logger.debug("debug")
logger.error("error")

输出

• 00:18:23.638 [DLOG] [DEBUG] <DLog.swift:19> debug
  1. 仅记录包含 "hello" 字符串的消息
let logger = DLog(.textPlain => .filter { $0.text().contains("hello") } => .stdout)

logger.debug("debug")
logger.log("hello world")
logger.info("info")

输出

• 00:19:17.821 [DLOG] [LOG] <DLog.swift:18> hello world
  1. 记录与特定作用域相关的消息
let filter = Filter { item in
    return item.scope?.name == "Load"
} isScope: { scope in
    return scope.name == "Load"
}

let logger = DLog(.textPlain => filter => .stdout)

logger.trace("trace")
logger.scope("Load") { scope1 in
    scope1.debug("debug")

    logger.scope("Parse") { scope2 in
        scope2.log("log")
        scope2.info("info")
    }

    scope1.error("error")
}
logger.fault("fault")

输出

• 16:28:50.443 [DLOG] ┌ [Load] 
• 16:28:50.443 [DLOG] ├ [DEBUG] <DLogTests.swift:528> debug
• 16:28:50.443 [DLOG] ├ [ERROR] <DLogTests.swift:535> error
• 16:28:50.443 [DLOG] └ [Load] (0.001s)

.disabled

这是一个共享的禁用记录器常量,它不会发出任何日志消息,当您想关闭某些构建配置、首选项、条件等的记录器时,它非常有用。

// Logging is enabled for `Debug` build configuration only

#if DEBUG
    let logger = DLog(.textPlain => .file(path))
#else
    let logger = DLog.disabled
#endif

对于禁用不必要的日志类别也可以这样做,而无需注释或删除记录器的函数。

//let netLogger = log["NET"]
let netLogger = DLog.disabled // Disable "NET" category

禁用的记录器会继续在作用域和间隔内运行您的代码。

let logger = DLog.disabled

logger.log("start")
logger.scope("scope") { scope in
    scope.debug("debug")

    print("scope code")
}
logger.interval("signpost") {
    logger.info("info")

    print("signpost code")
}
logger.log("finish")

输出

scope code
signpost code

配置

您可以通过设置应使用记录器中的哪些信息来自定义记录器的输出。 LogConfig 是一个根结构,用于配置记录器,其中包含日志消息的通用设置。

例如,您可以更改日志消息的默认视图,其中包括开始符号、类别、日志类型和位置

let logger = DLog()
logger.info("Info message")

输出

• 23:53:16.116 [DLOG] [INFO] <DLog.swift:12> Info message

更改为仅包含您的开始符号和时间戳的新外观

var config = LogConfig()
config.sign = ">"
config.options = [.sign, .time]

let logger = DLog(config: config)

logger.info("Info message")

输出

> 00:01:24.380 Info message

TraceConfig

它包含与 trace 方法相关的配置值,其中包括跟踪视图选项、线程和堆栈配置。

默认情况下,trace 方法使用 .compact 视图选项来生成有关当前函数名称和线程信息的信息

let logger = DLog()

func doTest() {
    logger.trace()
}

doTest()

输出

• 16:42:56.065 [DLOG] [TRACE] <DLogTests.swift:521> {func:doTest,thread:{name:main,number:1}}

但是您可以更改它以显示具有漂亮样式的函数和队列名称

var config = LogConfig()
config.traceConfig.options = [.function, .queue]
config.traceConfig.style = .pretty

let logger = DLog(config: config)

func doTest() {
    logger.trace()
}

doTest()

输出

• 10:17:59.021 [DLOG] [TRACE] <DLogTests.swift:180> {
  func : doTest,
  queue : com.apple.main-thread
}

ThreadConfig

跟踪配置具有 threadConfig 属性,可以更改线程信息的视图选项。 例如,记录器可以打印线程的当前 QoS。

var config = LogConfig()
config.traceConfig.threadConfig.options = [.number, .qos]

let logger = DLog(config: config)

func doTest() {
    logger.trace()
}

doTest()

DispatchQueue.global().async {
    doTest()
}

输出

• 10:18:40.976 [DLOG] [TRACE] <DLogTests.swift:179> {func:doTest,thread:{number:1,qos:userInteractive}}
• 10:18:40.978 [DLOG] [TRACE] <DLogTests.swift:179> {func:doTest,thread:{number:2,qos:userInitiated}}

StackConfig

trace 方法可以输出调用此方法时当前线程的调用堆栈回溯。 要启用此功能,您应该使用 stackConfig 属性配置堆栈视图选项、样式和深度

let logger: DLog = {
  var config = LogConfig()
  config.traceConfig.options = [.stack]
  config.traceConfig.stackConfig.options = [.symbol, .frame]
  config.traceConfig.stackConfig.depth = 3
  config.traceConfig.style = .pretty
  return DLog(config: config)
}()

func third() {
  logger.trace()
}

func second() {
  third()
}

func first() {
  second()
}

first()

输出

• 10:20:47.180 [DLOG] [TRACE] <DLogTests.swift:183> {
  stack : [
    {
      frame : 0,
      symbol : third #1 () -> () in DLogTests.DLogTests.test() -> ()
    },
    {
      frame : 1,
      symbol : second #1 () -> () in DLogTests.DLogTests.test() -> ()
    },
    {
      frame : 2,
      symbol : first #1 () -> () in DLogTests.DLogTests.test() -> ()
    }
  ]
}

注意:完整的调用堆栈回溯仅在调试模式下可用。

IntervalConfig

您可以使用 LogConfigintervalConfig 属性更改间隔统计信息的视图选项,以显示所需的信息,例如:.count.min.max 等。 或者您可以使用 .all 输出所有参数。

var config = LogConfig()
config.intervalConfig.options = [.all]

let logger = DLog(config: config)

logger.interval("signpost") {
    Thread.sleep(forTimeInterval: 3)
}

输出

• 10:21:47.407 [DLOG] [INTERVAL] <DLogTests.swift:178> {average:3.005s,count:1,duration:3.005s,max:3.005s,min:3.005s,total:3.005s} signpost

Objective-C

DLog 通过 DLogObjC 库向 Objective-C 公开所有功能,这在混合代码的项目中非常有用,因此您可以记录消息、创建作用域和间隔、共享全局记录器等。

日志级别

@import DLogObjC;

DLog* logger = [DLog new];

logger.log(@"log");
logger.trace(@"trace");
logger.debug(@"debug");
logger.info(@"info");
logger.warning(@"warning");
logger.error(@"error");
logger.assert(NO, @"assert");
logger.fault(@"fault");

您还可以格式化日志消息

logger.info(@"Version: %@, build: %d", @"1.2.0", 123);

输出

• 20:54:48.348 [DLOG] [INFO] <Test.m:16> Version: 1.2.0, build: 123

作用域(Scope)

LogScope* scope = logger.scope(@"Scope1", ^(LogScope* scope) {
    scope.debug(@"debug1");
});

// OR

scope = logger.scope(@"Scope2");
[scope enter];
scope.debug(@"debug2");
[scope leave];

间隔(Interval)

LogInterval* interval = logger.interval(@"interval", ^{
    [NSThread sleepForTimeInterval:0.25];
});

// OR

[interval begin];
[NSThread sleepForTimeInterval:0.25];
[interval end];

类别(Category)

LogProtocol* netLogger = logger[@"NET"];
netLogger.log(@"net logger");

管道(Pipeline)

DLog* logger = [[DLog alloc] initWithOutputs:@[LogOutput.textEmoji, LogOutput.stdOut]];
logger.info(@"info");

输出

• 14:17:07.306 [DLOG] ✅ [INFO] <Test.m:15> info

过滤器(Filter)

// Debug messages only
LogOutput* filter = [LogOutput filterWithItem:^BOOL(LogItem* logItem) {
    return logItem.type == LogTypeDebug;
}];

DLog* logger = [[DLog alloc] initWithOutputs:@[LogOutput.textPlain, filter, LogOutput.stdOut]];
logger.log(@"log");
logger.info(@"info");
logger.debug(@"debug");

输出

• 14:19:50.212 [DLOG] [DEBUG] <Test.m:21> debug

已禁用

DLog* logger = DLog.disabled;

安装

XCode 项目

  1. 选择 Xcode > File > Add Packages...
  2. 添加程序包资源库: https://github.com/ikhvorost/DLog.git
  3. 在您的源文件中导入程序包: import DLog

Swift Package

DLog 程序包依赖项添加到您的 Package.swift 文件

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/ikhvorost/DLog.git", from: "1.0.0")
    ],
    targets: [
        .target(name: "YourPackage",
            dependencies: [
                .product(name: "DLog", package: "DLog")
            ]
        ),
        ...
    ...
)

许可证

DLog 在 MIT 许可下可用。 有关更多信息,请参见LICENSE文件。

Donate