📈

swift-log入門:Logger/LogHandler/MetadataProvider

に公開

概要

apple/swift-logライブラリは、Appleが提供し、さまざまなライブラリから使用されている安定したロギングライブラリです。インターフェイスはシンプルで利用する際に考慮することはほとんどありませんが、この記事ではapple/swift-logの基本的なAPIについて紹介します。このAPIはapple/swift-metricsapple/swift-distributed-tracingなどのエコシステムでもほとんど同じAPIであるため、これを理解しておくことで他のライブラリの理解が容易になります。

https://github.com/apple/swift-log

https://github.com/apple/swift-metrics

https://github.com/apple/swift-distributed-tracing

apple/swift-log: Logging

apple/swift-logはApple公式のロギングのエコシステムです。以下のことができます。

  • 安定したインターフェイスでログを記録できる
  • LogHandlerでログの出力先を変更可能
  • MetadataProviderでログに情報を付加できる
  • 指定したログレベル未満のログを無視する

依存追加方法

Package.swift
// 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に設定されているMetadataProviderLogHandlerFactoryが使用されます。

またLoggingSystemではデフォルトで

  • LogHandler: StreamLogHandler.standardError(label: String)
  • MetadataProvider: nil

を使用します。

Sources/Playground/Playground.swift
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の初期化時にLogHandlerfactory関数を指定するか、LoggingSystem.bootstrap(factory: (String) -> any LogHandler)でデフォルトのLogHandlerを変更することによって使用するLogHandlerをカスタマイズすることができます。

特定のLoggerだけカスタマイズしたい場合には初期化時に、グローバルにカスタマイズしたい場合にはLoggingSystemで設定します。

Sources/Playground/Playground.swift
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のカスタマイズ

MetadataProviderLoggingSystem.bootstrapまたはLogger.initの際に指定することで使用することができます。MetadataProviderはデフォルト値がnilであるため、指定しない限りはメタデータはログに出力されません。主にトレースIDなどの情報の付与に使用されます。

Sources/Playground/Playground.swift
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の設定が行われています。

Sources/App/entrypoint.swift
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に書き込まれるようになります
  • MetadataProvidernilに設定されており、metadataはデフォルトで出力されません。
  • ログレベルは次の優先順で決まります
    1. 起動時の引数--logで指定されたLogLevel
    2. 環境変数または.envLOG_LEVEL
    3. デフォルトでproductionモードであればnoticeそうでなければinfo
  • デフォルトではログに以下の情報を空白区切りで出力します。
    • [ ラベル ] ※ログレベルがtrace以下の場合のみ出力
    • [ ログレベル(レベルによって色が変わる) ]
    • メッセージ
    • [ メタデータ(例: meta1: value1, meta2: value2) ]
    • (ファイル名:行番号) ※ログレベルがdebug以下の場合のみ出力

あとがき

普段気にしていませんでしたが、デフォルトのLoggerってStandard Error側に出るんですね。そして、Vapor使うとStandard Outputに出るんですね。初めて知りました。
LoggingSystemとかMetricsSystemとかではやはり@unchecked Sendableがよく使われていました。これらの設定が動的に変更するような場面はないので大丈夫なのでしょうし、互換性の都合上仕方がないものではありますが、Apple公式に使用されると少し切ない気持ちになりますね。

nextbeat Tech Blog

Discussion