Skip to content

Adopt swift-log task-local logger (Logger.current)#685

Open
sebsto wants to merge 4 commits into
mainfrom
task-local-logger
Open

Adopt swift-log task-local logger (Logger.current)#685
sebsto wants to merge 4 commits into
mainfrom
task-local-logger

Conversation

@sebsto

@sebsto sebsto commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

swift-log 1.14 added a task-local logger (SLG-0006). This wires it into the runtime so handlers can read Logger.current instead of threading context.logger through every function they call.

The runtime now binds the per-invocation logger as Logger.current around the handler call. Any code in the handler's call tree picks it up automatically, with the request ID and trace ID metadata already attached. context.logger keeps working exactly as before, and inside a handler the two are equivalent.

No public API breaks. This is purely additive:

  • New logger-free LambdaContext and LoggingConfiguration initializers that use Logger.current. The old logger-taking ones are deprecated and will go away in the next major.
  • Runtime initializer logger defaults changed from Logger(label: "...") to Logger.current, so a runtime built inside an app's withLogger(...) scope inherits that logger.

A note on toolchains: the async withLogger overload needs Swift 6.2, so the binding in the run loop is behind #if compiler(>=6.2). On 6.1 the handler still runs, just without the automatic Logger.current binding (context.logger is unaffected). We can drop the guard when 6.1 support goes away (when Swift 6.4 is GA). swift-log floor bumped to 1.14.0 in both Package files.

Examples and docs:

  • JSONLogging shows a helper function logging through Logger.current with no logger parameter.
  • ServiceLifecycle+Postgres binds the app logger once with withLogger at startup and reads Logger.current in its DB helpers.
  • New logging article in the DocC docs.

Tests cover the run loop binding the logger, metadata propagating through structured concurrency, and the new logger-free initializers.

@sebsto sebsto added the 🆕 semver/minor Adds new public API. label Jun 29, 2026
@sebsto sebsto self-assigned this Jun 29, 2026
@sebsto sebsto requested review from 0xTim and adam-fowler June 29, 2026 18:43
@sebsto

sebsto commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

@0xTim or @adam-fowler Can you have a look at this PR when you will have 1h ?
TL;DR: I'm trying to add support for swift-log task-local logger.

Feedback and comments welcome.
Thank you

@adam-fowler

Copy link
Copy Markdown
Collaborator

I've gone about this completely differently with Hummingbird. I've treated Hummingbird as a top level object that defines the logger (this is what it was before). Then I wrap each connection in a withLogger call setting the task local logger as defined by Hummingbird. This means systems that use the task local logger when called from Hummingbird routes will have the correct logger available to them. But Hummingbird itself does not use it. I see no advantage to using it internally.

For me moving to using the task local logger internally is a breaking change as it changes how you set the logger. You now have a confused API where you can set the logger either from the runtime configuration or via task locals. Which one takes precedence?

@sebsto

sebsto commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you @adam-fowler.

This PR doesn't change how users provide a logger. It's actually close to what you describe for Hummingbird: the runtime owns the logger definition, and wraps each invocation in a withLogger call that binds the per-request logger as the task-local, so downstream code can read Logger.current without threading context.logger everywhere. The runtime itself never reads Logger.current for its own diagnostics; it uses the logger stored at init (self.logger / loggingConfiguration.baseLogger).

The only behavioral change is the default value of the existing logger: parameter, from Logger(label: "LambdaRuntime") to Logger.current. Both ways of supplying a logger that worked before still work.

"which logger wins"?

There is only one input: the logger: initializer parameter. Logger.current is simply its default value. The runtime captures that parameter once at init time into loggingConfiguration.baseLogger and never consults the task-local again. So there are not two competing precedence levels at runtime.

That said, your underlying concern is fair: because the default is Logger.current, building a runtime inside an enclosing withLogger { ... } scope will silently pick up that logger as the base. That is intentional (it's how a lib like ServiceLifecycle can define the logger once), but it does mean the task-local influences the runtime logger at construction time. It's a default-value behavior, not a second configuration channel.

The resolving order at init time is like this:

  1. Explicit logger: argument if you pass one.
  2. Otherwise Logger.current (which is the process-wide default if no withLogger scope is active).
  3. Env vars (AWS_LAMBDA_LOG_LEVEL / LOG_LEVEL, AWS_LAMBDA_LOG_FORMAT) then override level and format on top of whatever base logger was chosen.

Per invocation, the runtime derives a request logger from that base (adding requestID / traceID) and binds it as Logger.current for the handler scope.

The automatic Logger.current binding requires Swift 6.2+ because the async withLogger overload isn't available before that, so the binding is behind #if compiler(>=6.2). On 6.1 the handler still runs and context.logger is unaffected, but Logger.current in a called function falls back to the process-wide default (no request metadata). We can drop the guard when 6.1 support goes away (Swift 6.4 GA).

There are two approaches to use a logger, let the runtime manage the logger or provide my own logger.

Approach 1: let the runtime manage the logger

Before this PR:

let runtime = LambdaRuntime { (event: Request, context: LambdaContext) in
    context.logger.info("Processing request")
    helperFunction(event, logger: context.logger)  // logger threaded manually
}
try await runtime.run()

After this PR (Swift 6.2+), the same code works, plus Logger.current is bound per invocation:

let runtime = LambdaRuntime { (event: Request, context: LambdaContext) in
    context.logger.info("Processing request")  // unchanged
    helperFunction(event)                       // no logger argument needed
}
try await runtime.run()

func helperFunction(_ event: Request) {
    Logger.current.debug("Validating")  // picks up requestID / traceID automatically (Swift 6.2+)
}

Approach 2: provide my own logger

Unchanged before and after: the explicit parameter still wins:

var myLogger = Logger(label: "my-function")
myLogger.logLevel = .trace
let runtime = LambdaRuntime(logger: myLogger) { (event: Request, context: LambdaContext) in
    context.logger.info("Processing request")
}
try await runtime.run()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🆕 semver/minor Adds new public API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants