|
| 1 | +# Task local logger |
| 2 | + |
| 3 | +Authors: [Franz Busch](https://github.com/FranzBusch) |
| 4 | + |
| 5 | +## Introduction |
| 6 | + |
| 7 | +Swift Structured Concurrency provides first class capabilities to propagate data |
| 8 | +down the task tree via task locals. This provides an amazing opportunity for |
| 9 | +structured logging. |
| 10 | + |
| 11 | +## Motivation |
| 12 | + |
| 13 | +Structured logging is a powerful tool to build logging message that contain |
| 14 | +contextual metadata. This metadata is often build up over time by adding more to |
| 15 | +it the more context is available. A common example for this are request ids. |
| 16 | +Once a request id is extracted it is added to the loggers metadata and from that |
| 17 | +point onwards all log messages contain the request id. This improves |
| 18 | +observability and debuggability. The current pattern to do this in `swift-log` |
| 19 | +looks like this: |
| 20 | + |
| 21 | +```swift |
| 22 | +func handleRequest(_ request: Request, logger: Logger) async throws { |
| 23 | + // Extract the request id to the metadata of the logger |
| 24 | + var logger = logger |
| 25 | + logger[metadataKey: "request.id"] = "\(request.id)" |
| 26 | + |
| 27 | + // Importantly we have to pass the new logger forward since it contains the request id |
| 28 | + try await sendResponse(logger: logger) |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +This works but it causes significant overhead due to passing of the logger |
| 33 | +through all methods in the call stack. Furthermore, sometimes it is impossible to pass |
| 34 | +a logger to some methods if those are protocol requirements like `init(from: Decoder)`. |
| 35 | + |
| 36 | +Swift Structured Concurrency introduced the concept of task locals which |
| 37 | +propagate down the structured task tree. This fits perfectly with how we expect |
| 38 | +logging metadata to accumulate and provide more information the further down the |
| 39 | +task tree we get. |
| 40 | + |
| 41 | +## Proposed solution |
| 42 | + |
| 43 | +I propose to add a new task local definition to `Logger`. Adding this task local |
| 44 | +inside the `Logging` module provides the one canonical task local that all other |
| 45 | +packages in the ecosystem can use. |
| 46 | + |
| 47 | +```swift |
| 48 | +extension Logger { |
| 49 | + /// The task local logger. |
| 50 | + /// |
| 51 | + /// It is recommended to use this logger in applications and libraries that use Swift Concurrency |
| 52 | + /// instead of passing around loggers manually. |
| 53 | + @TaskLocal |
| 54 | + public static var logger: Logger |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +The default value for this logger is going to be a `SwiftLogNoOpLogHandler()`. |
| 59 | + |
| 60 | +Applications can then set the task local logger similar to how they currently bootstrap |
| 61 | +the logging backend. If no library in the proccess is creating its own logger it is even possible |
| 62 | +to not use the normal bootstrapping methods at all and fully rely on structured concurrency for |
| 63 | +propagating the logger and its metadata. |
| 64 | + |
| 65 | +```swift |
| 66 | +static func main() async throws { |
| 67 | + let logger = Logger(label: "Logger") { StreamLogHandler.standardOutput(label: $0)} |
| 68 | + |
| 69 | + Logger.$logger.withValue(logger) { |
| 70 | + // Run your application code |
| 71 | + try await application.run() |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +Places that want to log can then just access the task local and produce a log message. |
| 77 | + |
| 78 | +```swift |
| 79 | +Logger.logger.info("My log message") |
| 80 | +``` |
| 81 | + |
| 82 | +Adding additional metadata to the task local logger is as easy as updating the logger |
| 83 | +and binding the task local value again. |
| 84 | + |
| 85 | +```swift |
| 86 | +Logger.$logger.withValue(logger) { |
| 87 | + Logger.logger.info("First log") |
| 88 | + |
| 89 | + var logger = Logger.logger |
| 90 | + logger[metadataKey: "MetadataKey1"] = "Value1" |
| 91 | + Logger.$logger.withValue(logger) { |
| 92 | + Logger.logger.info("Second log") |
| 93 | + } |
| 94 | + |
| 95 | + Logger.logger.info("Third log") |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +Running the above code will produce the following output: |
| 100 | + |
| 101 | +``` |
| 102 | +First log |
| 103 | +MetadataKey1=Value1 Second log |
| 104 | +Third log |
| 105 | +``` |
| 106 | + |
| 107 | +## Alternatives considered |
| 108 | + |
| 109 | +### Provide static log methods |
| 110 | + |
| 111 | +Instead of going through the task local `Logger.logger` to emit log messages we |
| 112 | +could add new static log methods like `Logger.log()` or `Logger.info()` that |
| 113 | +access the task local internally. This is soemthing that we can do in the future |
| 114 | +as an enhancement but isn't required initially. |
0 commit comments