Using swift-log with swift-testing, how to bootstrap logging system

My code uses swift-log to generate log messages.

My unit tests have been successfully migrated to swift-testing.

I am on latest swift 6.0 with strict concurrency checking.

Now I want to bootstrap a global LoggingSystem with LogHandler for all my unit tests so that I can start logging.

import Foundation
import Testing
import Logging

let logger: Logger = {
    LoggingSystem.bootstrap { label in
        var handler = StreamLogHandler.standardOutput(label: label)
        // handler.logLevel = .debug
        return handler
    }
    return Logger(label: "logger")
}()

final class FunctionalityTests {

    init() async throws {
        // make sure logger + logger handler is initialised before being used
        logger.trace("starting")
    }

    @Test func a() async throws {
        // ...
    }

    @Test func b() async throws {
        // ...
    }

    // ... and so on ...
}

Now I have multiple tests in this class. And all of them want to log. And all of them want to share one underlying LogHandler.

I understand that swift-testing allocates multiple instances of the FunctionalityTests class and invokes a randomly selected test on each of those. They all share the same process. And they all run in parallel when invoked from the command line via swift test.

I thought code above would be safe, but it's crashing randomly with:

Logging/Logging.swift:617: Precondition failed: logging system can only be initialized once per process.

This would indicate that global let logger = {}() is being called multiple times.

But this is in swift 6 with strict mode enabled.
I thought this is never going to happen. Compiler is silent.

Help me please understand what I'm doing wrong. And how to properly initialise Logging for swift-testing.

thanks for any tips,
Martin

P.S. I found this bug filed against swift-testing which I think hits the nail: test process bootstrap/async setup hook · Issue #328 · swiftlang/swift-testing · GitHub, but I'd still like to understand why is the code above not working. Shouldn't the global variable logger be available once per process and initialised only once? What magic is happening here?

OK, answering my own question. My Package contains multiple TestSuites, all of them using the let logger pattern above. So there is not just one logger but multiple of them scoped per each test file.

Since they all share the same process the bootstrap is called multiple times.

The root cause found, but what is the nice and clean solution?

The solution depends on whether or not you want them all to share a single logger.

If they should all share a single logger, declare let logger only once (perhaps in a Helpers.swift file?) in your test target.

If they should all use separate loggers, consider setting it as an instance property of your test suite:

final class FunctionalityTests {
    let logger = Logger(label: "logger") { label in
        var handler = StreamLogHandler.standardOutput(label: label)
        // handler.logLevel = .debug
        return handler
    }

    init() async throws {
        // make sure logger + logger handler is initialised before being used
        logger.trace("starting")
    }

    // ...
}

Thanks @grynspan for your help.

Your workaround of using instance based let logger works nicely, but as it uses custom handler, while not overriding the the system one, any newly created loggers will not use that handler and fallback to the system one, and will not share the logLevel we want.

So as long as I can propagate the logger instance variable down to my classes, everything is fine, but the moment some component creates its own Logger(label: "xxx") it will not inherit the custom handler and log level set.

for that I think I need to call LoggingSystem.bootstrap only once, perhaps in Helpers.swift file as you suggested, which needs to be turned into its own target in order for the testTargets to depend on it... which calls for its own package so that I can reuse it in the future....

that starts to be rather convoluted to just call a method once and setup global logging :slight_smile:

Back to test process bootstrap/async setup hook · Issue #328 · swiftlang/swift-testing · GitHub