|
| 1 | +# 003: Accepting loggers in libraries |
| 2 | + |
| 3 | +Accept loggers through method parameters to ensure proper metadata propagation. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Libraries should accept logger instances through method parameters rather than |
| 8 | +storing them as instance variables. This practice ensures metadata (such as |
| 9 | +correlation IDs) is properly propagated down the call stack, while giving |
| 10 | +applications control over logging configuration. |
| 11 | + |
| 12 | +### Motivation |
| 13 | + |
| 14 | +When libraries accept loggers as method parameters, they enable automatic |
| 15 | +propagation of contextual metadata attached to the logger instance. This is |
| 16 | +especially important for distributed systems where correlation IDs and tracing |
| 17 | +information must flow through the entire request processing pipeline. |
| 18 | + |
| 19 | +### Example |
| 20 | + |
| 21 | +#### Recommended: Accept logger through method parameters |
| 22 | + |
| 23 | +```swift |
| 24 | +// ✅ Good: Logger passed through method parameters |
| 25 | +struct RequestProcessor { |
| 26 | + func processRequest(_ request: HTTPRequest, logger: Logger) async throws -> HTTPResponse { |
| 27 | + // Add metadata to the logger that every log statement should contain |
| 28 | + var logger = logger |
| 29 | + logger[metadataKey: "request.method"] = "\(request.method)" |
| 30 | + logger[metadataKey: "request.path"] = "\(request.path)" |
| 31 | + logger[metadataKey: "request.id"] = "\(request.id)" |
| 32 | + |
| 33 | + logger.debug("Processing request") |
| 34 | + |
| 35 | + // Pass logger down to maintain metadata context |
| 36 | + let validatedData = try validateRequest(request, logger: logger) |
| 37 | + let result = try await executeBusinessLogic(validatedData, logger: logger) |
| 38 | + |
| 39 | + logger.debug("Request processed successfully") |
| 40 | + return result |
| 41 | + } |
| 42 | + |
| 43 | + private func validateRequest(_ request: HTTPRequest, logger: Logger) throws -> ValidatedRequest { |
| 44 | + logger.debug("Validating request parameters") |
| 45 | + // Validation logic with same logger context |
| 46 | + return ValidatedRequest(request) |
| 47 | + } |
| 48 | + |
| 49 | + private func executeBusinessLogic(_ data: ValidatedRequest, logger: Logger) async throws -> HTTPResponse { |
| 50 | + logger.debug("Executing business logic") |
| 51 | + |
| 52 | + // Further propagate to other services |
| 53 | + let dbResult = try await databaseService.query(data.query, logger: logger) |
| 54 | + |
| 55 | + logger.debug("Business logic completed") |
| 56 | + return HTTPResponse(data: dbResult) |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +#### Alternative: Accept logger through initializer when appropriate |
| 62 | + |
| 63 | +```swift |
| 64 | +// ✅ Acceptable: Logger through initializer for long-lived components |
| 65 | +final class BackgroundJobProcessor { |
| 66 | + private let logger: Logger |
| 67 | + |
| 68 | + init(logger: Logger) { |
| 69 | + self.logger = logger |
| 70 | + } |
| 71 | + |
| 72 | + func processJob(_ job: Job) async { |
| 73 | + // For background jobs, method parameter approach is still preferred |
| 74 | + // to allow per-job metadata |
| 75 | + var logger = logger |
| 76 | +logger[metadataKey: "job.id"] = "\(job.id)" |
| 77 | + |
| 78 | + await processJobInternal(job, logger: logger) |
| 79 | + } |
| 80 | + |
| 81 | + private func processJobInternal(_ job: Job, logger: Logger) async { |
| 82 | + logger.debug("Processing job") |
| 83 | + // Process with contextual logger |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +#### Avoid: Libraries creating their own loggers |
| 89 | + |
| 90 | +Libraries might create their own loggers; however, this leads to two problems. |
| 91 | +First, the user of the library can't inject their own loggers which means he has |
| 92 | +no control in customizing the log level or log handler. Secondly, it breaks the |
| 93 | +metadata propagation since the user can't pass in a logger with already attached |
| 94 | +metadata. |
| 95 | + |
| 96 | +```swift |
| 97 | +// ❌ Bad: Library creates its own logger |
| 98 | +final class MyLibrary { |
| 99 | + private let logger = Logger(label: "MyLibrary") // Loses all context |
| 100 | +} |
| 101 | + |
| 102 | +// ✅ Good: Library accepts logger from caller |
| 103 | +final class MyLibrary { |
| 104 | + func operation(logger: Logger) { |
| 105 | + // Maintains caller's context and metadata |
| 106 | + } |
| 107 | +} |
| 108 | +``` |
0 commit comments