1- # 003: Accepting loggers in libraries
1+ # 003: Logger propagation in libraries
22
3- Accept loggers through method parameters to ensure proper metadata propagation.
3+ Propagate caller context by accepting a ` Logger ` parameter or reading the task-local
4+ `` Logger/current `` — never by constructing your own logger.
45
56## Overview
67
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.
8+ Libraries should obtain a ` Logger ` in one of two ways: accept one through a method or
9+ initializer parameter, or read `` Logger/current `` from the task-local. Both approaches
10+ preserve the caller's metadata, log level, and handler choice. Constructing a logger
11+ inside a library — via `` Logger/init(label:) `` — takes those choices away from the
12+ application and breaks metadata propagation.
1113
1214### Motivation
1315
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 must flow
17- through the entire request processing pipeline.
16+ The application controls logging: which backend, which log level, which metadata
17+ travels with each request. Libraries that participate in this picture obtain a logger
18+ the application has set up, rather than constructing their own from scratch. This
19+ ensures correlation IDs and other contextual metadata flow through the entire call
20+ stack, and gives the application a single place to redirect or filter all log output.
21+
22+ Two propagation mechanisms are available, and they coexist. Choose the explicit
23+ parameter when the library's API already accepts a ` Logger ` (or it's natural to add
24+ one) — the call site stays declarative about what gets logged where. Choose the
25+ task-local when adding a ` logger: ` parameter would pollute an API that otherwise has
26+ no logging concern in its signature. Application code drives the task-local binding;
27+ library code reads it.
1828
1929### Example
2030
@@ -31,70 +41,141 @@ struct RequestProcessor {
3141 logger[metadataKey : " request.id" ] = " \( request.id ) "
3242
3343 logger.debug (" Processing request" )
34-
44+
3545 // Pass the logger down to maintain metadata context.
3646 let validatedData = try validateRequest (request, logger : logger)
3747 let result = try await executeBusinessLogic (validatedData, logger : logger)
38-
48+
3949 logger.debug (" Request processed successfully" )
4050 return result
4151 }
42-
52+
4353 private func validateRequest (_ request : HTTPRequest, logger : Logger) throws -> ValidatedRequest {
4454 logger.debug (" Validating request parameters" )
45- // Include validation logic that uses the same logger context.
4655 return ValidatedRequest (request)
4756 }
48-
57+
4958 private func executeBusinessLogic (_ data : ValidatedRequest, logger : Logger) async throws -> HTTPResponse {
5059 logger.debug (" Executing business logic" )
51-
52- // Further propagate the logger to other services.
5360 let dbResult = try await databaseService.query (data.query , logger : logger)
54-
5561 logger.debug (" Business logic completed" )
5662 return HTTPResponse (data : dbResult)
5763 }
5864}
5965```
6066
61- #### Alternative : Accept logger through initializer when appropriate
67+ #### Recommended : Accept logger through initializer for long-lived components
6268
6369``` swift
6470// ✅ Acceptable: Logger through initializer for long-lived components
6571final class BackgroundJobProcessor {
6672 private let logger: Logger
67-
73+
6874 init (logger : Logger) {
6975 self .logger = logger
7076 }
71-
77+
7278 func run () async {
73- // Execute some long running work
7479 logger.debug (" Update about long running work" )
75- // Execute some more long running work
7680 }
7781}
7882```
7983
84+ #### Recommended: Read `` Logger/current `` from the task-local
85+
86+ When a ` logger: ` parameter would clutter an otherwise logger-unrelated API, read the
87+ task-local. The application's accumulated metadata (` request.id ` , etc.) flows in
88+ automatically without an explicit hand-off.
89+
90+ ``` swift
91+ // ✅ Good: Library reads Logger.current; caller scopes context via withLogger.
92+ public struct AnalyticsClient {
93+ public func track (_ event : String ) {
94+ Logger.current .info (" event" , metadata : [" event.name" : " \( event ) " ])
95+ }
96+ }
97+
98+ // Application binds at @main and scopes per-request metadata.
99+ @main
100+ struct MyServer {
101+ static func main () async throws {
102+ let logger = Logger (label : " my-server" )
103+ try await withLogger (logger) { _ in
104+ try await runServices ()
105+ }
106+ }
107+ }
108+
109+ func handleRequest (_ req : HTTPRequest) async throws {
110+ try await withLogger (mergingMetadata : [" request.id" : " \( req.id ) " ]) { _ in
111+ AnalyticsClient ().track (" request.received" ) // sees request.id automatically
112+ }
113+ }
114+ ```
115+
116+ For per-statement metadata, pass it via the ` metadata: ` parameter on the log call —
117+ it does not propagate and does not leak into other code:
118+
119+ ``` swift
120+ Logger.current .info (" step" , metadata : [" step.name" : " validate" ])
121+ ```
122+
123+ For a few back-to-back lines, take a local copy and stamp metadata there — also no
124+ propagation:
125+
126+ ``` swift
127+ var local = Logger.current
128+ local[metadataKey : " step.name" ] = " validate"
129+ local.info (" entering" )
130+ local.info (" done" )
131+ ```
132+
80133#### Avoid: Libraries creating their own loggers
81134
82- Libraries might create their own loggers; however, this leads to two problems.
83- First, users of the library can't inject their own loggers which means they have
84- no control in customizing the log level or log handler. Secondly, it breaks the
85- metadata propagation since users can't pass in a logger with already attached
86- metadata.
135+ Constructing `` Logger/init(label:) `` inside a library takes control over the handler, log
136+ level, and base metadata away from the application. The application cannot redirect,
137+ filter, or silence the library's output.
87138
88139``` swift
89- // ❌ Bad: Library creates its own logger
140+ // ❌ Bad: Library creates its own logger — loses caller's context.
90141final class MyLibrary {
91- private let logger = Logger (label : " MyLibrary" ) // Loses all context
142+ private let logger = Logger (label : " MyLibrary" )
92143}
144+ ```
93145
94- // ✅ Good: Library accepts logger from caller
95- final class MyLibrary {
96- func operation (logger : Logger) {
97- // Maintains caller's context and metadata
98- }
146+ `` Logger/init(label:) `` is for the application — typically at ` @main ` , paired with
147+ `` LoggingSystem/bootstrap(_:) `` . Library code, including internal application modules,
148+ should not construct loggers.
149+
150+ #### Avoid: Relying on `` Logger/current `` across non-Task boundaries
151+
152+ `` Logger/current `` is backed by a ` TaskLocal ` and propagates through Swift's structured
153+ concurrency model only. Callbacks invoked on non-Task threads — GCD blocks,
154+ ` URLSession ` completion handlers, delegate methods dispatched onto specific queues,
155+ ` NotificationCenter ` observers, C-API callbacks — see the * default* fallback logger,
156+ not the bound one. Metadata bound by the calling ` Task ` is invisible inside those
157+ callbacks.
158+
159+ ``` swift
160+ // ❌ Bad: completion handler runs without the Task context; Logger.current is the fallback.
161+ try await withLogger (mergingMetadata : [" request.id" : " r1" ]) { _ in
162+ URLSession.shared .dataTask (with : req) { data, _ , _ in
163+ Logger.current .info (" response" ) // empty-label fallback, no request.id
164+ }.resume ()
165+ }
166+ ```
167+
168+ For libraries with async completion-handler APIs, accept an explicit ` Logger ` parameter.
169+ If capturing and rebinding across the boundary is the only option:
170+
171+ ``` swift
172+ // ✅ Good: capture before the boundary, rebind on the other side.
173+ try await withLogger (mergingMetadata : [" request.id" : " r1" ]) { _ in
174+ let captured = Logger.current
175+ URLSession.shared .dataTask (with : req) { data, _ , _ in
176+ withLogger (captured) { logger in
177+ logger.info (" response" ) // request.id preserved
178+ }
179+ }.resume ()
99180}
100181```
0 commit comments