-
Notifications
You must be signed in to change notification settings - Fork 301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce task local logger #315
Open
FranzBusch
wants to merge
1
commit into
apple:main
Choose a base branch
from
FranzBusch:fb-task-local-logger
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/configuration/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc | ||
.benchmarkBaselines/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift Logging API open source project | ||
// | ||
// Copyright (c) 2024 Apple Inc. and the Swift Logging API project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of Swift Logging API project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Benchmark | ||
import Logging | ||
|
||
let benchmarks = { | ||
let defaultMetrics: [BenchmarkMetric] = [ | ||
.mallocCountTotal, | ||
.instructions, | ||
.wallClock, | ||
] | ||
|
||
Benchmark( | ||
"NoOpLogger", | ||
configuration: Benchmark.Configuration( | ||
metrics: defaultMetrics, | ||
scalingFactor: .mega, | ||
maxDuration: .seconds(10_000_000), | ||
maxIterations: 100 | ||
) | ||
) { benchmark in | ||
let logger = Logger(label: "Logger", SwiftLogNoOpLogHandler()) | ||
|
||
for _ in 0..<benchmark.scaledIterations.upperBound { | ||
logger.info("Log message") | ||
} | ||
} | ||
|
||
Benchmark( | ||
"NoOpLogger task local", | ||
configuration: Benchmark.Configuration( | ||
metrics: defaultMetrics, | ||
scalingFactor: .mega, | ||
maxDuration: .seconds(10_000_000), | ||
maxIterations: 100 | ||
) | ||
) { benchmark in | ||
let logger = Logger(label: "Logger", SwiftLogNoOpLogHandler()) | ||
|
||
Logger.$logger.withValue(logger) { | ||
for _ in 0..<benchmark.scaledIterations.upperBound { | ||
Logger.logger.info("Log message") | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// swift-tools-version: 5.8 | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "benchmarks", | ||
platforms: [ | ||
.macOS("14") | ||
], | ||
dependencies: [ | ||
.package(path: "../"), | ||
.package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.22.0"), | ||
], | ||
targets: [ | ||
.executableTarget( | ||
name: "LoggingBenchmarks", | ||
dependencies: [ | ||
.product(name: "Benchmark", package: "package-benchmark"), | ||
.product(name: "Logging", package: "swift-log"), | ||
], | ||
path: "Benchmarks/LoggingBenchmarks", | ||
plugins: [ | ||
.plugin(name: "BenchmarkPlugin", package: "package-benchmark") | ||
] | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift Logging API open source project | ||
// | ||
// Copyright (c) 2024 Apple Inc. and the Swift Logging API project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of Swift Logging API project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
import XCTest | ||
import Logging | ||
|
||
final class TaskLocalLoggerTests: XCTestCase { | ||
func test() async { | ||
let logger = Logger(label: "TestLogger") { StreamLogHandler.standardOutput(label: $0) } | ||
|
||
Logger.$logger.withValue(logger) { | ||
Logger.logger.info("Start log") | ||
var logger = Logger.logger | ||
logger[metadataKey: "MetadataKey1"] = "Value1" | ||
logger.logLevel = .trace | ||
Logger.$logger.withValue(logger) { | ||
Logger.logger.info("Log2") | ||
} | ||
Logger.logger.info("End log") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# Task local logger | ||
|
||
Authors: [Franz Busch](https://github.com/FranzBusch) | ||
|
||
## Introduction | ||
|
||
Swift Structured Concurrency provides first class capabilities to propagate data | ||
down the task tree via task locals. This provides an amazing opportunity for | ||
structured logging. | ||
|
||
## Motivation | ||
|
||
Structured logging is a powerful tool to build logging message that contain | ||
contextual metadata. This metadata is often build up over time by adding more to | ||
it the more context is available. A common example for this are request ids. | ||
Once a request id is extracted it is added to the loggers metadata and from that | ||
point onwards all log messages contain the request id. This improves | ||
observability and debuggability. The current pattern to do this in `swift-log` | ||
looks like this: | ||
|
||
```swift | ||
func handleRequest(_ request: Request, logger: Logger) async throws { | ||
// Extract the request id to the metadata of the logger | ||
var logger = logger | ||
logger[metadataKey: "request.id"] = "\(request.id)" | ||
|
||
// Importantly we have to pass the new logger forward since it contains the request id | ||
try await sendResponse(logger: logger) | ||
} | ||
``` | ||
|
||
This works but it causes significant overhead due to passing of the logger | ||
through all methods in the call stack. Furthermore, sometimes it is impossible to pass | ||
a logger to some methods if those are protocol requirements like `init(from: Decoder)`. | ||
|
||
Swift Structured Concurrency introduced the concept of task locals which | ||
propagate down the structured task tree. This fits perfectly with how we expect | ||
logging metadata to accumulate and provide more information the further down the | ||
task tree we get. | ||
|
||
## Proposed solution | ||
|
||
I propose to add a new task local definition to `Logger`. Adding this task local | ||
inside the `Logging` module provides the one canonical task local that all other | ||
packages in the ecosystem can use. | ||
|
||
```swift | ||
extension Logger { | ||
/// The task local logger. | ||
/// | ||
/// It is recommended to use this logger in applications and libraries that use Swift Concurrency | ||
/// instead of passing around loggers manually. | ||
@TaskLocal | ||
public static var logger: Logger | ||
} | ||
``` | ||
|
||
The default value for this logger is going to be a `SwiftLogNoOpLogHandler()`. | ||
|
||
Applications can then set the task local logger similar to how they currently bootstrap | ||
the logging backend. If no library in the proccess is creating its own logger it is even possible | ||
to not use the normal bootstrapping methods at all and fully rely on structured concurrency for | ||
propagating the logger and its metadata. | ||
|
||
```swift | ||
static func main() async throws { | ||
let logger = Logger(label: "Logger") { StreamLogHandler.standardOutput(label: $0)} | ||
|
||
Logger.$logger.withValue(logger) { | ||
// Run your application code | ||
try await application.run() | ||
} | ||
} | ||
``` | ||
|
||
Places that want to log can then just access the task local and produce a log message. | ||
|
||
```swift | ||
Logger.logger.info("My log message") | ||
``` | ||
|
||
Adding additional metadata to the task local logger is as easy as updating the logger | ||
and binding the task local value again. | ||
|
||
```swift | ||
Logger.$logger.withValue(logger) { | ||
Logger.logger.info("First log") | ||
|
||
var logger = Logger.logger | ||
logger[metadataKey: "MetadataKey1"] = "Value1" | ||
Logger.$logger.withValue(logger) { | ||
Logger.logger.info("Second log") | ||
} | ||
|
||
Logger.logger.info("Third log") | ||
} | ||
``` | ||
|
||
Running the above code will produce the following output: | ||
|
||
``` | ||
First log | ||
MetadataKey1=Value1 Second log | ||
Third log | ||
``` | ||
|
||
## Alternatives considered | ||
|
||
### Provide static log methods | ||
|
||
Instead of going through the task local `Logger.logger` to emit log messages we | ||
could add new static log methods like `Logger.log()` or `Logger.info()` that | ||
access the task local internally. This is soemthing that we can do in the future | ||
as an enhancement but isn't required initially. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this syntax required for passing metadata through to child tasks? I thought that was automatic. E.g.:
Definitely agree with renaming
logger
here - the API doesn't come across great with the double logger in this exampleThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logger is a value type, so the modified copy needs to be written back into the task local to be propagated to child tasks.