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 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
- Official Repository: https://github.com/apple/swift-log
- Swift Package Index: https://swiftpackageindex.com/apple/swift-log
- API Documentation: README in repository
- Swift Evolution: Proposals and discussions about logging API
- Server-Side Swift: https://swift.org/server/
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
)