Swift-Log

Apple's official open-source Swift logging API. Specialized for server-side Swift and cross-platform development, provides standardization of logging implementations. High compatibility with server-side frameworks like SwiftNIO and Vapor.

logging librarySwiftserver-sideAPIabstractionmetadataprovider model

Logging Library

swift-log

Overview

swift-log is the official logging API for Swift. It plays a crucial role especially in the Server-Side Swift ecosystem, providing an abstraction layer that allows different libraries and packages to log to a common logging destination. Through the provider model, flexible backend selection is possible, and metadata support and automatic addition of context information make log correlation in distributed systems easier.

Details

swift-log was developed by Apple Inc. in 2019 as a unified logging API for Swift. Unlike OSLog for iOS/macOS applications, it is primarily designed for use in Server-Side Swift. It provides a mechanism that allows libraries and frameworks to avoid depending on their own log implementations, enabling applications to freely choose log backends.

Technical Features

  • Abstraction API: Unified interface independent of backends
  • LogHandler Protocol: Implementation of custom log backends
  • Metadata Support: Attachment of structured key-value pairs
  • MetadataProvider: Automatic addition of context information
  • Log Levels: trace, debug, info, notice, warning, error, critical
  • Thread-Safe: Safe usage in multithreaded environments
  • Performance Optimization: Efficient log processing with minimal overhead

Architecture

  • Logger: Application-side log API
  • LogHandler: Abstraction of backend implementation
  • LoggingSystem: Global configuration and bootstrap
  • MetadataProvider: Automatic provision of context information

Supported Backends

  • StreamLogHandler: Standard output/error output
  • File Backends: Various file output implementations
  • Logstash: Integration with ELK stack
  • SwiftNIO: Asynchronous log processing
  • CloudWatch: AWS integration
  • Elasticsearch: Searchable log store

Pros and Cons

Pros

  • Standardization: Standard logging API in Swift Server ecosystem
  • Flexibility: Free backend selection and switching
  • Library Compatibility: Easy integration with third-party libraries
  • Metadata Support: Complete support for structured logging
  • Distributed Tracing Support: Correlated logs through Baggage integration
  • Performance: Efficient implementation and lazy evaluation
  • Official Support: Development and maintenance by Apple Inc.

Cons

  • iOS/macOS Limitations: Feature limitations and performance degradation compared to direct OSLog usage
  • Abstraction Cost: Slight overhead due to indirect layer
  • Privacy Control: OSLog privacy features not available
  • Learning Curve: Understanding of metadata providers and Baggage required
  • Backend Dependencies: Additional backend implementations required for practical features

Reference Links

Usage Examples

Basic Usage

import Logging

// System initialization (once at application startup)
LoggingSystem.bootstrap(StreamLogHandler.standardOutput)

// Create logger
let logger = Logger(label: "com.example.MyApp")

// Basic log output
logger.info("Hello World!") 
logger.error("Houston, we have a problem: \(problem)")

// Usage examples for each log level
logger.trace("Detailed trace information")
logger.debug("Debug information")
logger.info("General information")
logger.notice("Noteworthy information")
logger.warning("Warning message")
logger.error("An error occurred")
logger.critical("Critical error")

Using Metadata

import Logging

// Adding static metadata
var logger = Logger(label: "com.example.WebServer")
logger[metadataKey: "request-uuid"] = "\(UUID())"
logger[metadataKey: "user-id"] = "12345"

// Log with metadata (includes existing metadata)
logger.info("hello world")
// Output example: 2019-03-13T18:30:02+0000 info: request-uuid=F8633013-3DD8-481C-9256-B296E43443ED user-id=12345 hello world

// Adding inline metadata
logger.info("Product fetched.", metadata: ["productId": "42"])
logger.info("Product purchased.", metadata: ["paymentMethod": "apple-pay"])

// Multiple metadata operations
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"
])

Custom LogHandler Implementation

import Logging
import Foundation

// Custom log handler implementation
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)"
        
        // File output, network transmission, database storage, etc.
        print(output)
        
        // File log example
        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 }
    }
}

// System initialization
LoggingSystem.bootstrap(MyLogHandler.init)

// Usage example
let logger = Logger(label: "custom-logger")
logger.info("Log output with custom handler")

Utilizing MetadataProvider and Baggage

import Logging
// import Tracing  // swift-distributed-tracing required
// import ServiceContextModule  // ServiceContext required

// Tracing metadata provider
extension Logger.MetadataProvider {
    static let tracing = Logger.MetadataProvider {
        // Get tracing information from Baggage
        guard let serviceContext = ServiceContext.current else {
            return [:]
        }
        
        var metadata: Logger.Metadata = [:]
        
        // Add trace ID
        if let traceID = serviceContext.traceID {
            metadata["trace-id"] = "\(traceID)"
        }
        
        // Add span ID
        if let spanID = serviceContext.spanID {
            metadata["span-id"] = "\(spanID)"
        }
        
        // Add user ID
        if let userID = serviceContext.userID {
            metadata["user-id"] = "\(userID)"
        }
        
        return metadata
    }
}

// Set metadata provider during system initialization
LoggingSystem.bootstrap(
    metadataProvider: .tracing,
    StreamLogHandler.standardOutput
)

// Or set metadata provider for individual logger
let logger = Logger(
    label: "traced-logger", 
    metadataProvider: .tracing
)

// Execution with context
ServiceContext.$current.withValue(someServiceContext) {
    logger.info("Product fetched.")  
    // Output: [trace-id: abc123, span-id: def456] Product fetched.
    
    logger.info("Product purchased.", metadata: ["amount": "99.99"])
    // Output: [trace-id: abc123, span-id: def456, amount: 99.99] Product purchased.
}

Web Server Implementation Example

import Logging
import Vapor  // Using Vapor framework

// Logger setup per request in middleware
struct LoggingMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        // Generate unique ID per request
        let requestID = UUID()
        
        // Add request information to logger
        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"
        
        // Set updated logger to request
        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
        }
    }
}

// Logger usage in service layer
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
        }
    }
}

// Application configuration
func configure(_ app: Application) throws {
    // Logging system initialization
    LoggingSystem.bootstrap { label in
        var handler = StreamLogHandler.standardOutput(label: label)
        handler.logLevel = .debug
        return handler
    }
    
    // Register middleware
    app.middleware.use(LoggingMiddleware())
    
    // Route configuration
    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)
    }
}

Multi-Provider and Performance Optimization

import Logging

// Combine multiple metadata providers
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
    ])
}

// Performance-optimized log handler
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) {
        
        // Asynchronous log processing (doesn't block main thread)
        queue.async {
            let logEntry = self.formatLogEntry(
                level: level,
                message: message,
                metadata: metadata,
                source: source,
                file: file,
                function: function,
                line: line
            )
            
            // Implement batch processing or buffering
            self.writeLog(logEntry)
        }
    }
    
    private func formatLogEntry(level: Logger.Level, 
                              message: Logger.Message, 
                              metadata: Logger.Metadata?, 
                              source: String, 
                              file: String, 
                              function: String, 
                              line: UInt) -> String {
        // Efficient format processing
        var result = ""
        result.reserveCapacity(256)  // Pre-allocate memory
        
        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) {
        // Actual log output (file, network, etc.)
        print(entry)
    }
    
    subscript(metadataKey key: String) -> Logger.Metadata.Value? {
        get { metadata[key] }
        set { metadata[key] = newValue }
    }
}

// Optimized system initialization
LoggingSystem.bootstrap(
    metadataProvider: .combined,
    OptimizedLogHandler.init
)