swift-log入門:Logger/LogHandler/MetadataProvider
概要
apple/swift-logライブラリは、Appleが提供し、さまざまなライブラリから使用されている安定したロギングライブラリです。インターフェイスはシンプルで利用する際に考慮することはほとんどありませんが、この記事ではapple/swift-logの基本的なAPIについて紹介します。このAPIはapple/swift-metricsやapple/swift-distributed-tracingなどのエコシステムでもほとんど同じAPIであるため、これを理解しておくことで他のライブラリの理解が容易になります。
apple/swift-log: Logging
apple/swift-logはApple公式のロギングのエコシステムです。以下のことができます。
- 安定したインターフェイスでログを記録できる
-
LogHandlerでログの出力先を変更可能 -
MetadataProviderでログに情報を付加できる - 指定したログレベル未満のログを無視する
依存追加方法
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "swift-log-playground",
platforms: [.macOS(.v15)],
dependencies: [
+ .package(url: "https://github.com/apple/swift-log", from: "1.0.0")
],
targets: [
.executableTarget(
name: "Playground",
dependencies: [
+ .product(name: "Logging", package: "swift-log"),
],
swiftSettings: swiftSettings,
)
],
swiftLanguageModes: [.v6],
)
var swiftSettings: [SwiftSetting] {
[
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("NonescapableTypes"),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InternalImportsByDefault"),
]
}
デフォルトの挙動
デフォルトでは、標準エラー(standard error)にログが書き込まれます。
Label.init(label:)で初期化した場合には、LoggingSystemに設定されているMetadataProviderとLogHandlerのFactoryが使用されます。
またLoggingSystemではデフォルトで
-
LogHandler:StreamLogHandler.standardError(label: String) -
MetadataProvider: nil
を使用します。
import Logging
@main
enum Playground {
static func main() async throws {
let logger = Logger(label: "default")
logger.info("デフォルトログ")
// 2025-10-05T21:47:15+0900 info default: [Playground] デフォルトログ
}
}
ログの形式は以下のものが空白区切りになっています
時刻(%Y-%m-%dT%H:%M:%S%z)ログレベル(trace|debug|info|notice|warning|error|critical)-
ラベル名: キー順でソートされたメタデータ(例: key1=value1 key2=value2 key3=value3)- [
ソース] メッセージ
LogHandlerのカスタマイズ
Loggerの初期化時にLogHandlerのfactory関数を指定するか、LoggingSystem.bootstrap(factory: (String) -> any LogHandler)でデフォルトのLogHandlerを変更することによって使用するLogHandlerをカスタマイズすることができます。
特定のLoggerだけカスタマイズしたい場合には初期化時に、グローバルにカスタマイズしたい場合にはLoggingSystemで設定します。
import Logging
@main
enum Playground {
static func main() async throws {
struct CustomLogHandler: LogHandler {
var label: String
var metadata: Logger.Metadata
var logLevel: Logger.Level
subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? {
get { metadata[key] }
set { metadata[key] = newValue }
}
func log(
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt
) {
print("❤️ level", level)
print("🧡 message", message)
print("💛 metadata", metadata?.description ?? "<none>")
print("💚 source", source)
print("🩵 file", file)
print("💙 function", function)
print("💜️ line", line)
}
}
LoggingSystem.bootstrap { label in
CustomLogHandler(label: label, metadata: ["meta": "data"], logLevel: .debug)
}
let logger = Logger(label: "custom")
logger.info("カスタムログ")
/*
❤️ level info
🧡 message カスタムログ
💛 metadata <none>
💚 source Playground
🩵 file Playground/Playground.swift
💙 function main()
💜️ line 36
*/
}
}
インターフェイスから、ログ出力時には、以下のものが得られることがわかります。
- ログレベル(trace|debug|info|notice|warning|error|critical)
- メッセージ
- メタデータ(
[String: MetadataValue]) - ソース(モジュール名)
- file(ファイルパス)
- function(関数名)
- line(行番号)
MetadataProviderのカスタマイズ
MetadataProviderもLoggingSystem.bootstrapまたはLogger.initの際に指定することで使用することができます。MetadataProviderはデフォルト値がnilであるため、指定しない限りはメタデータはログに出力されません。主にトレースIDなどの情報の付与に使用されます。
import Logging
@main
enum Playground {
@TaskLocal static var metadata = Logger.Metadata()
static func main() async throws {
let metadataProvider = Logger.MetadataProvider { metadata }
LoggingSystem.bootstrap(
{ label, provider in
StreamLogHandler.standardOutput(label: label, metadataProvider: metadataProvider)
},
metadataProvider: metadataProvider
)
let logger = Logger(label: "custom")
$metadata.withValue([
"user_id": "1",
"teams": ["a", "b", "c"],
"roles": ["a": "admin", "b": "normal"],
]) {
logger.info("メタデータログ")
// 2025-10-05T22:18:36+0900 info custom: roles=["a": "admin", "b": "normal"] teams=["a", "b", "c"] user_id=1 [Playground] メタデータログ
}
}
}
VaporにおけるLoggingSystem
Vaporの標準テンプレートでは以下のようなコードでLoggingSystemの設定が行われています。
import Vapor
import Logging
import NIOCore
import NIOPosix
@main
enum Entrypoint {
static func main() async throws {
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
// ....
}
}
Vapor 4では、ログの出力に
-
vapor/console-kitを使用して標準出力 standard outputに書き込まれるようになります -
MetadataProviderはnilに設定されており、metadataはデフォルトで出力されません。 - ログレベルは次の優先順で決まります
- 起動時の引数
--logで指定されたLogLevel - 環境変数または
.envのLOG_LEVEL - デフォルトで
productionモードであればnoticeそうでなければinfo
- 起動時の引数
- デフォルトではログに以下の情報を空白区切りで出力します。
- [
ラベル] ※ログレベルがtrace以下の場合のみ出力 - [
ログレベル(レベルによって色が変わる)] メッセージ- [
メタデータ(例: meta1: value1, meta2: value2)] - (
ファイル名:行番号) ※ログレベルがdebug以下の場合のみ出力
- [
あとがき
普段気にしていませんでしたが、デフォルトのLoggerってStandard Error側に出るんですね。そして、Vapor使うとStandard Outputに出るんですね。初めて知りました。
LoggingSystemとかMetricsSystemとかではやはり@unchecked Sendableがよく使われていました。これらの設定が動的に変更するような場面はないので大丈夫なのでしょうし、互換性の都合上仕方がないものではありますが、Apple公式に使用されると少し切ない気持ちになりますね。
Discussion