Logging and structured concurrency (TaskLocal)

Goal

I want to let people customise the logger that will be used in my library at any given time. In particular, the client may want to have some global metadata added to all logs from my lib for calls from a specific part of their app, or depending on the app state (while processing a request for instance, add the request ID in the metadata of the logs).

First the obvious solution

I can simply make all the public methods in my lib have an additional argument: the logger. That way the client has full control on the logger.

I’ve heard about @TaskLocal

And I’m wondering if it wouldn’t be better to use this! IIUC a TaskLocal variable is specifically designed to pass some metadata state of the application while in a Task. Which is exactly what I want.

So here’s how it would go (1st draft):

enum LibConf {
   @TaskLocal
   public var logger: Logger = .init(label: "com.my-company.my-lib")
}

And the client just has to do this to change the logger during a task:

LibConf.$logger.withValue(newLogger, operation: {
   ...
})

Great. But now let’s imagine we are using 10 libs that use this concept. So the caller has to specifically call the withValue function for all of the libs!

This is not maintainable…

A bit further

So I’ve got another idea. We keep the same Conf object, but we give a way to set _logger directly!

enum LibConf {
   @TaskLocal
   public var logger: Logger = .init(label: "com.my-company.my-lib")
   public static func setLogger(wrappedLogger: TaskLocal<Logger>) {
      _logger = wrappedLogger
   }
}

Now the client can create his TaskLocal logger, then set the logger of my lib to the exact same TaskLocal:

enum ClientConf {
   @TaskLocal
   var logger: Logger = .init(label: "com.his-company.awesome-executable")
}
/* Note: We should use _logger here, but in this particular case
 *       the projected value is the wrapper itself (and _logger
 *       is private). */
Lib1Conf.setLogger(wrappedLogger: ClientConf.$logger)
Lib2Conf.setLogger(wrappedLogger: ClientConf.$logger)
...

Now there is only one TaskLocal to change to change all the loggers.

Question 1: Is this completely crazy or does it seem reasonable? I’ve tried it, it seems to work and I do not see any obvious downsides. Are there issues I would have missed with this solution?

Annnd even further!

So we now got a way to bind all the loggers together. This is great!

But then all the loggers have the same label…

Question 2: Is it an issue? In my head the label of a Logger is mainly the same concept as the subsystem of os.log on macOS. Usually the subsystem for my logs is the bundle ID of the module I’m working in. Thus it feels weird to have all the logs coming from the same label.

Question 3: How to have a logger with a different label, but with the metadata still bound to the current task? I’ve searched but did not find any elegant solution.

Thanks!

PS: Should the logger property be optional? If the client wants to disable logging in a module, it gives him an easy way to do it by setting the logger to nil