Swift-Log
Apple公式のオープンソースSwiftロギングAPI。サーバーサイドSwiftとクロスプラットフォーム開発に特化し、ロギング実装の標準化を提供。SwiftNIOやVaporなどのサーバーサイドフレームワークとの親和性が高い。
ロギングライブラリ
swift-log
概要
swift-logは、Swift用の公式ロギングAPIです。特にServer-Side Swiftエコシステムで重要な役割を果たし、抽象化レイヤーを提供することで、異なるライブラリやパッケージが共通のロギング先にログを出力できます。プロバイダーモデルにより柔軟なバックエンド選択が可能で、メタデータサポートとコンテキスト情報の自動追加により、分散システムでのログ相関が容易になります。
詳細
swift-logは、Apple社により2019年に開発されたSwift向けの統一ロギングAPIです。iOS/macOSアプリ向けのOSLogとは異なり、主にServer-Side Swiftでの利用を想定して設計されています。ライブラリやフレームワークが独自のログ実装に依存せず、アプリケーション側でログバックエンドを自由に選択できる仕組みを提供します。
技術的特徴
- 抽象化API: バックエンドに依存しない統一インターフェース
- LogHandlerプロトコル: カスタムログバックエンドの実装
- メタデータサポート: 構造化されたキー・値ペアの添付
- MetadataProvider: 自動的なコンテキスト情報の追加
- ログレベル: trace、debug、info、notice、warning、error、critical
- スレッドセーフ: マルチスレッド環境での安全な利用
- パフォーマンス最適化: 効率的なログ処理と最小限のオーバーヘッド
アーキテクチャ
- Logger: アプリケーション側のログAPI
- LogHandler: バックエンド実装の抽象化
- LoggingSystem: グローバル設定とブートストラップ
- MetadataProvider: コンテキスト情報の自動提供
対応バックエンド
- StreamLogHandler: 標準出力/エラー出力
- ファイルバックエンド: 各種ファイル出力実装
- Logstash: ELKスタック連携
- SwiftNIO: 非同期ログ処理
- CloudWatch: AWS連携
- Elasticsearch: 検索可能ログストア
メリット・デメリット
メリット
- 標準化: Swift Server生態系での標準ロギングAPI
- 柔軟性: 自由なバックエンド選択と切り替え
- ライブラリ親和性: サードパーティライブラリとの統合が容易
- メタデータサポート: 構造化ログの完全サポート
- 分散トレーシング対応: Baggageとの統合による相関ログ
- パフォーマンス: 効率的な実装とレイジー評価
- 公式サポート: Apple社による開発と保守
デメリット
- iOS/macOS制限: OSLog直接使用に比べて機能制限とパフォーマンス低下
- 抽象化コスト: 間接層によるわずかなオーバーヘッド
- プライバシー制御: OSLogのプライバシー機能が利用不可
- 学習コスト: メタデータプロバイダーやBaggageの理解が必要
- バックエンド依存: 実用的な機能には追加のバックエンド実装が必要
参考ページ
- 公式リポジトリ: https://github.com/apple/swift-log
- Swift Package Index: https://swiftpackageindex.com/apple/swift-log
- API ドキュメント: リポジトリ内のREADME
- Swift Evolution: ロギングAPIに関する提案と議論
- Server-Side Swift: https://swift.org/server/
書き方の例
基本的な使用方法
import Logging
// システムの初期化(アプリケーション開始時に一度だけ)
LoggingSystem.bootstrap(StreamLogHandler.standardOutput)
// ロガーの作成
let logger = Logger(label: "com.example.MyApp")
// 基本的なログ出力
logger.info("Hello World!")
logger.error("Houston, we have a problem: \(problem)")
// 各ログレベルの使用例
logger.trace("詳細な追跡情報")
logger.debug("デバッグ情報")
logger.info("一般的な情報")
logger.notice("注目すべき情報")
logger.warning("警告メッセージ")
logger.error("エラーが発生")
logger.critical("致命的なエラー")
メタデータの使用
import Logging
// 静的メタデータの追加
var logger = Logger(label: "com.example.WebServer")
logger[metadataKey: "request-uuid"] = "\(UUID())"
logger[metadataKey: "user-id"] = "12345"
// メタデータ付きログ(既存のメタデータも含まれる)
logger.info("hello world")
// 出力例: 2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED user-id=12345 hello world
// インラインメタデータの追加
logger.info("Product fetched.", metadata: ["productId": "42"])
logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"])
// 複数のメタデータ操作
logger[metadataKey: "subsystem"] = "networking"
logger[metadataKey: "category"] = "http-client"
logger.info("HTTP request completed", metadata: [
"status_code": "200",
"response_time_ms": "150",
"endpoint": "/api/v1/users"
])
カスタムLogHandlerの実装
import Logging
import Foundation
// カスタムログハンドラーの実装
struct MyLogHandler: LogHandler {
var logLevel: Logger.Level = .info
var metadata: Logger.Metadata = [:]
private let label: String
init(label: String) {
self.label = label
}
func log(level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let combinedMetadata = self.metadata.merging(metadata ?? [:]) { _, new in new }
var output = "[\(timestamp)] [\(level)] [\(label)]"
if !combinedMetadata.isEmpty {
let metadataString = combinedMetadata
.map { "\($0.key)=\($0.value)" }
.joined(separator: " ")
output += " [\(metadataString)]"
}
output += " \(message)"
// ファイル出力、ネットワーク送信、データベース保存等
print(output)
// ファイルログの例
if let data = (output + "\n").data(using: .utf8) {
let fileURL = URL(fileURLWithPath: "/tmp/my-app.log")
try? data.append(to: fileURL)
}
}
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get { metadata[key] }
set { metadata[key] = newValue }
}
}
// システムの初期化
LoggingSystem.bootstrap(MyLogHandler.init)
// 使用例
let logger = Logger(label: "custom-logger")
logger.info("カスタムハンドラーでのログ出力")
MetadataProviderとBaggageの活用
import Logging
// import Tracing // swift-distributed-tracingが必要
// import ServiceContextModule // ServiceContextが必要
// トレーシング用メタデータプロバイダー
extension Logger.MetadataProvider {
static let tracing = Logger.MetadataProvider {
// Baggageからトレーシング情報を取得
guard let serviceContext = ServiceContext.current else {
return [:]
}
var metadata: Logger.Metadata = [:]
// トレースIDの追加
if let traceID = serviceContext.traceID {
metadata["trace-id"] = "\(traceID)"
}
// スパンIDの追加
if let spanID = serviceContext.spanID {
metadata["span-id"] = "\(spanID)"
}
// ユーザーIDの追加
if let userID = serviceContext.userID {
metadata["user-id"] = "\(userID)"
}
return metadata
}
}
// システム初期化時にメタデータプロバイダーを設定
LoggingSystem.bootstrap(
metadataProvider: .tracing,
StreamLogHandler.standardOutput
)
// または、個別ロガーでメタデータプロバイダーを設定
let logger = Logger(
label: "traced-logger",
metadataProvider: .tracing
)
// コンテキスト付きでの実行
ServiceContext.$current.withValue(someServiceContext) {
logger.info("Product fetched.")
// 出力: [trace-id: abc123, span-id: def456] Product fetched.
logger.info("Product purchased.", metadata: ["amount": "99.99"])
// 出力: [trace-id: abc123, span-id: def456, amount: 99.99] Product purchased.
}
Webサーバーでの実装例
import Logging
import Vapor // Vaporフレームワークを使用
// ミドルウェアでリクエストごとのロガー設定
struct LoggingMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
// リクエストごとのユニークID生成
let requestID = UUID()
// ロガーにリクエスト情報を追加
var logger = request.logger
logger[metadataKey: "request-id"] = "\(requestID)"
logger[metadataKey: "method"] = "\(request.method)"
logger[metadataKey: "path"] = "\(request.url.path)"
logger[metadataKey: "user-agent"] = request.headers.first(name: .userAgent) ?? "unknown"
// 更新されたロガーをリクエストに設定
request.logger = logger
let startTime = Date()
logger.info("Request started")
do {
let response = try await next.respond(to: request)
let duration = Date().timeIntervalSince(startTime)
logger.info("Request completed", metadata: [
"status": "\(response.status.code)",
"duration_ms": "\(Int(duration * 1000))"
])
return response
} catch {
let duration = Date().timeIntervalSince(startTime)
logger.error("Request failed", metadata: [
"error": "\(error)",
"duration_ms": "\(Int(duration * 1000))"
])
throw error
}
}
}
// サービス層でのロガー使用
struct UserService {
let logger: Logger
func createUser(name: String, email: String) async throws -> User {
logger.info("Creating user", metadata: [
"name": "\(name)",
"email": "\(email)"
])
do {
let user = try await database.create(User(name: name, email: email))
logger.info("User created successfully", metadata: [
"user_id": "\(user.id)"
])
return user
} catch {
logger.error("Failed to create user", metadata: [
"error": "\(error)"
])
throw error
}
}
}
// アプリケーション設定
func configure(_ app: Application) throws {
// ロギングシステムの初期化
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
handler.logLevel = .debug
return handler
}
// ミドルウェア登録
app.middleware.use(LoggingMiddleware())
// ルート設定
app.post("users") { req async throws -> User in
let userService = UserService(logger: req.logger)
let userData = try req.content.decode(CreateUserRequest.self)
return try await userService.createUser(name: userData.name, email: userData.email)
}
}
マルチプロバイダーとパフォーマンス最適化
import Logging
// 複数のメタデータプロバイダーを組み合わせ
extension Logger.MetadataProvider {
static let environment = Logger.MetadataProvider {
return [
"environment": ProcessInfo.processInfo.environment["ENVIRONMENT"] ?? "development",
"hostname": ProcessInfo.processInfo.hostName,
"process_id": "\(ProcessInfo.processInfo.processIdentifier)"
]
}
static let application = Logger.MetadataProvider {
return [
"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown",
"build_number": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown"
]
}
static let combined = Logger.MetadataProvider.multiplex([
.environment,
.application,
.tracing
])
}
// パフォーマンス最適化されたログハンドラー
struct OptimizedLogHandler: LogHandler {
var logLevel: Logger.Level = .info
var metadata: Logger.Metadata = [:]
private let label: String
private let queue = DispatchQueue(label: "logging-queue", qos: .utility)
init(label: String) {
self.label = label
}
func log(level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt) {
// 非同期でログ処理(メインスレッドをブロックしない)
queue.async {
let logEntry = self.formatLogEntry(
level: level,
message: message,
metadata: metadata,
source: source,
file: file,
function: function,
line: line
)
// バッチ処理またはバッファリングを実装
self.writeLog(logEntry)
}
}
private func formatLogEntry(level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt) -> String {
// 効率的なフォーマット処理
var result = ""
result.reserveCapacity(256) // 事前にメモリ確保
let timestamp = Date().timeIntervalSince1970
result += "[\(timestamp)] [\(level)] "
let combinedMetadata = self.metadata.merging(metadata ?? [:]) { _, new in new }
if !combinedMetadata.isEmpty {
result += "[\(combinedMetadata)] "
}
result += "\(message)"
return result
}
private func writeLog(_ entry: String) {
// 実際のログ出力(ファイル、ネットワーク等)
print(entry)
}
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
get { metadata[key] }
set { metadata[key] = newValue }
}
}
// 最適化されたシステム初期化
LoggingSystem.bootstrap(
metadataProvider: .combined,
OptimizedLogHandler.init
)