[Feedback] Server Logging API (with revisions)


(Johannes Weiss) #28

So you're right that there's no way to make a compile-time decision but I would argue that this is not necessary. Let me explain why: The reason we have struct Logger with everything @inlinable is that the only thing we need to do if we don't log is check the log level, everything else should be behind a branch in the compiled binary.

In other words, let's imagine you wrote

logger.debug("hello", metadata: ["foo": "bar", "buz": "qux"])

what the compiler will emit into the calling function (because @inlinable) is something more akin to this

if logger.logHandler.logLevel <= .debug {
    /* do stuff to prepare the log message, like evaluate the autoclosures etc */
    logger.logHandler.log(.debug, "hello", ["foo": "bar", ...])
}

So the idea is that all the expensive stuff is only done iff the comparison to the log level returns true. To show you what I mean, let's have a look at the generated control flow graph (from the assembly) for this function:

@inline(never)
func foobar(logger: Logger) {
    logger.debug("hello", metadata: ["foo": "bar", "buz": "qux"])
}

So as you can see, we have a prologue (the purple box), then a branch to the left which does loads of stuff and a branch to the right which goes straight into the epilogue (bottom box). So what we want is that if nothing is logged, we go straight from the purple box to the bottom one. So let's zoom into the purple box:

So what decides if we go left (expensive) or right (cheap) is the result of call _$S7LoggingAAO5LevelO1loiySbAD_ADtFZ which (when passed through swift demangle) revels itself as static Logging.Logging.Level.< infix(Logging.Logging.Level, Logging.Logging.Level) -> Swift.Bool.

Pretty good! We only get the log level from the LogHandler existential and then invoke the Logging.Level's less than function. So we don't do anything with the @autoclosures if we don't log which is pretty good.

I realise that a little work is still more than no work at all, how important do you think this would be? Because in production you'll run Swift in release mode because debug is quite very slow :).

I'm totally happy with this. Happy to either make a change to the proposal or if accepted this can be a PR because IMHO it clearly doesn't change the spirit of the proposal at all. Please let me know what you think.

I think it's good if we enforce that everybody passes some string there to identify themselves as I reckon more serious loggers than StdoutLogger might want to do something with the information and if we make it optional there's just be many nils in there I suggest. But I'm not married to not having it optional, just don't see any real benefit.

That I like a lot more and actually think we should do this. Let's see if anyone has objections here.

Nope, that can all be inlined. As you can see in the assembly below, it's just one load from a static address, just like StaticString.

That's a good question :slight_smile:. I consider StdoutLogger more a toy at this point in time that we should finish when/if this goes live . But as a general question we should document what merge strategy we think is useful. I haven't thought about it too much yet. If anyone has opinions, please let us know :slight_smile:.

Thank you very much for your great feedback.


(Matthew Johnson) #29

Absolutely! My goal is to try and convince you that this is not the best way forward by making a compelling case. All I ask is that you consider the argument I make with an open mind. :slight_smile:

I don't disagree, but I think there are other ways to be compatible with it than to just impose a pervasive global logging facility on everyone.

Can you explain why you think it is necessary to make a pervasively visible global logger in order to allow those who wish to use one in their library to be able to do so? An alternative path would be for such libraries to provide a top-level configuration API to inject a logger which they make available as an internal global within their library. This approach allows the library to use a globally visible logger in their code without polluting the global scope of every other module.

Perhaps you're right - looking again it looks like maybe it is just the handler that is global. However, this is just as problematic IMO, perhaps even more-so! The proposal even says "Libraries should never change the logging implementation as that should owned by the application".

If you use a design based on derived loggers instead of a global handler, logger factory, and Logging.bootstrap function then it simply wouldn't be possible for anyone to stomp on the handler. Code would use the logger provided to it and / or new loggers derived from it. Libraries would just need to provide a configuration point to receive the top-level logger instance from which other loggers are derived.

The handler is unknown to the code that is creating the logger. I think I missed the fact that you are only supporting a single global handler when I wrote this though. That is also unfortunate IMO. If you use derived loggers it wouldn't be that hard to support multiple back-ends and there are clear semantics for the handler used by a derived logger: it is the same handler as the logger from which it is derived. This would allow an application to use different handlers for different subsystems, for example.

There are other design approaches that could address these use cases without requiring everyone to accept the ability to set the log level everywhere in their code. If you are receptive to considering alternatives I would be happy to give this further thought and provide a concrete suggestion.

I am not convinced compromises to the semantics of the API itself are necessary in order to meet the requirements at hand. Perhaps small compromises (such as exposing a configuration point for the top-level logger a library uses instead of the library just calling Logger.make) are necessary, but I think they are reasonable and I hope people would find them acceptable.

Thanks for explaining that. I was not as familiar with the differences as I should have been. :blush:

I definitely support the goals project in general, I just don't want to see it get locked into a design that makes some significant (and IMO unnecessary) compromises. Once people start using this there will be compatibility concerns which will make significant semantic changes difficult or impossible to make.

Exactly. myLogger continues to behave exactly as it did prior to that call, while myChangedLogger exhibits the modified behavior: additional metadata, additional layer to its "name", perhaps a modified level, etc.

Ideally the ability to set the level when deriving a new logger is not something that would be available on all loggers. There are a number of different behaviors people might want here and quite a few options for how to implement an API flexible enough to support the various use cases. I would be happy to spend time making more concrete suggestions if this is a direction you are willing to explore.


(Neal Lester) #30

Making the logger label optional could encourage library authors to provide a nil which would then be problematic for the consumers of that library. There is also the issue of potential clashes between different libraries. An explicit convention for the label could help with that or perhaps having the label default to the module name.


(Johannes Weiss) #31

:+1:

Okay, there it is our misunderstanding. When I say global logger, I mean a global facility that allows you to emit log messages. The proposed API does not offer that. What you meant with global logger is the Logging.bootstrap which is indeed public but that isn't a logging facility, that is a (meant to be one-time) selection of the logging backend implementation.

IMHO, one important thing for logging infrastructures is that all parts of the program (including all libraries) by default log to the same logging backend, so that all messages end up for example in the same file, in the correct order etc.

One way to achieve that (and that seems to be both your any my preferred way) is to explicitly hand loggers around to code that logs. But that is at odds with people just wanting to log from anywhere and I don't believe that we can ignore those people.

If I understand your proposed solution correctly, somewhere in the main function of the program a 'root logger' is constructed and from there on it's passed around and the logger (or derivations of it) has to reach all code that intends to log. Is that correct? And if yes: How would then a library that doesn't want to change its API log to the same destination as the rest of the code?

Btw, I really think we should provide a public API to make a Logger from a LogHandler which doesn't require any globals for testing/special cases etc. But I still expect all real-world code that wants to create a logger out of thin air (and these users really exist) to go through the global.

Yes.

It's quite easy to create a multiplexing LogHandler that sends everything to multiple LogHandlers, we do in fact now ship one now because it was a somewhat common request.
You are right however that with the current API it's hard to support different logging backends for different sub-systems. That's because I didn't think that is very compelling but if we just added an API to create a Logger from a LogHandler that would be a very easy solution catering to the people that really want that.

Obviously, that doesn't take your main issue (the globally configured logging implementation) away but I really don't think we can do away with it.

Thanks, I would indeed be very interested to see more concrete thoughts on this.

Here is where it gets tricky. Convincing the library authors is one thing that's hard but even harder is convincing application developers to configure each and every library with the right Logger/LogHandler. Or would you want to also design and ship a dependency injection framework?

Okay, after reading everything you wrote, am I right in thinking that what you dislike basically boils down to two issues:

  1. the global logger creation facility that has only one (can be multiplexed though) logging backend configured globally
  2. using mutating properties to change the logger configuration instead of derivation

We've talked (and misunderstood each other) a lot about (1) and my position remains that not being able to make up loggers out of thin air is necessary. I would be very very happy if I were wrong on that case because I really dislike globals of that sort and believe those things should be explicit. However what's IMHO more important than my personal taste is that the ecosystem gains a logging solution that is very widely adopted. It's all OSS and the ecosystem is young and used to changes so I believe that over time we can establish best practices and eventually maybe even get rid of that global.

I find (2) quite interesting however. What you call derived loggers is I believe pretty much what I called value-typed LogHandlers. Assuming a value-typed LogHandler, there is no difference between

let originalLogger = ...
let changedLogger = originalLogger.addMetadata("foo", "bar")
changedLogger.debug("hello world")

and

let originalLogger = ...
var changedLogger = originalLogger
changedLogger[metadataKey: "foo"] = "bar"
changedLogger.debug("hello world")

in both cases, changedLogger is a derivation from originalLogger and originalLogger remains unchanged. I like the look and feel of second version better. But obviously as you have pointed out before and as the proposal says, the semantics of the mutation aren't clear here. originalLogger will only be unchanged iff the configured LogHandler has value semantics.
I'm sure this make us both feel quite uneasy but after many discussions I have accepted this as a necessary tradeoff. And I can totally understand if you don't. But there'll be people who want to change the log level globally throughout the whole application etc and with the current design you can make a LogHandler which will allow you to do that.


(Johannes Weiss) #32

Agreed. Unfortunately, the module name is afaik not available, just #function, #file, #line, #column. But I like @IanPartridge's idea to default it to "\(#file):\(#line)" I think.


(Ravi Kandhadai Madhavan) #33

@johannesweiss @IanPartridge It is great to see the progress on the server-side logging API design, and the discussions here. As we (@Ben_Cohen, @moiseev, @Devin_Coughlin and I)
are working on improving the Apple's OS logging API of Swift, there appears to be a lot of synergies in the ideas and goals between these proposals. We think it would be great if we can keep open the possibility of making these APIs source-level compatible in the future. Just wondering if we can think about and smooth out some of the divergences that are likely to affect compatibility or hinder flexibility in the future.

One specific question we have is whether it is better to use custom string interpolation in place of strings in the server-side logging API. Currently, log functions such as log, trace, debug, info are accepting Strings. Accepting a custom string interpolation type (similar to what we plan to do for os_log) will offer more flexibility in the future. Initially, the custom string interpolation can just build strings (like DefaultStringInterpolation), but they can be customized in the future. Some benefits of using a custom string interpolation type are

  • It enables specifying formatting options, in addition to what is supported by DefaultStringInterpolation, for interpolated values. E.g. like specifying precision with floating-point values, using hex/octal notations etc. It could also support rending options specific to a logging backend.

  • It can potentially enable many compiler optimizations, which are much easier to do when the static and dynamic parts of the string interpolation are not combined together. In fact, this also relates to @IanPartridge's comments on eliminating compile-time overheads when logging is disabled. These kind of optimizations are something that we plan to do for os_log APIs, and they are possible because of custom string interpolation.

  • It is easier to introduce and customize privacy policies. Some parts of the interpolation can be treated as private. It also enables localization .

  • They provide an elegant way to extend and customize the log APIs to custom types. For instance, the author of a library or framework can extend the string interpolation type and add support for logging their types. They can also implicitly attach metadata and tags, while hiding it from the users of the log calls. (This OS Log proposal provides an example of extending that API to log a user-defined type Developer.)

Also, it is better to avoid overloading the APIs to accept a custom string interpolation type (in the future), as the compiler should then make a choice between using the DefaultStringInterpolation type vs the custom string interpolation type on seeing a string interpolation. To enforce a particular behavior users of the logging API have to add explicit types to the log messages (or do type casting) at all places, which is potentially error prone. It seems like it is better to make this choice of using custom string interpolation vs strings upfront.


(Ben Cohen) #34

In general, I think it's important that the general look and feel of the new "device" and "server" logging APIs be as similar as possible. This doesn't necessarily mean that they are the same API or that we need to come up with some grand unified logging system for both devices and servers – but the APIs should at least feel similar.


(Johannes Weiss) #35

Thanks for your great explanation!

Yes, I think that's a good idea. In the previous discussion thread I already mentioned that we would leverage the features that come for os_log. Therefore I definitely think we should also adopt custom string interpolation instead of plain Strings.

Do you think we should do this now or wait for the Apple's OS logging API to land and then use the same/similar type?


(Jacob Williams) #36

I actually think it would be weird if they weren’t the same. I dont think anyone would expect there to be 2 different logging infrastructures in a language. That would be actively confusing since now you have to think about and learn when each one is appropriate or available and how to use each one.

Of course, it may not be feasible to expect the entire server side community to wait for the Apple guys to propose/redesign/implement a new logging infrustructure.

Thta being said, would it be possible to come to a common consensus around the base logging functionality and then the Apple guys just build a LogHandler (like the StdoutHandler) which would be a SystemHandler or ConsoleHandler that would log to wherever os_log is writing its logs?


(Neal Lester) #37

This could introduce difficulties. The label may be used by application developers to configure the log handler (e.g. vary loglevel by subsystem) so it is really part of the API and should be designed. Most libraries would probably use a single value, and if partitioning a library's logging space were appropriate is seems unlikely #file would be an appropriate axis for doing so. #file by itself would provide no real protection for inter-library clashes (I'm assuming that the file name isn't fully qualified). Adding #line produces a value which will change arbitrarily from release to release and still provides relatively weak protection from clashes.

I think module name is included in the output of type:of. Granted its a computation; but if one is creating so many loggers that the computation becomes a performance problem then provide a constant or maybe don't create so many loggers.

On the other hand, maybe no default with an easily followed convention defined in the library documentation which "guarantees" no clashes (e.g. "github.com/weissi/swift-server-logging-api-proposal" or "github.com/weissi/swift-server-logging-api-proposal/sub-subsystem" would be better in that it forces consideration of what is actually an API design decision in a way that the usual case won't take much effort for library authors. Adding the convention of defining the value(s) in public static constants (perhaps with names also defined by convention) provides some protection against breaking log handler factories by changing string values.


(Matthew Johnson) #38

Agree.

I think that's a best practice but I am not advocating everyone be required to do that. What I am suggesting is that if they don't want to they need to set up a globally visible logger and / or logger factory in their own code.

If a library is 100% rigid on not changing API and has no existing mechanism for injecting a logging facility that could be adapted to work with the proposed logging API then clearly it cannot. But IMO that is an unreasonably rigid position for a library to take. A library wishing to interact with a logging ecosystem controlled by it's application context should be required to support a mechanism for injecting a logger. If desired, the library could make that logger and / or a factory that derives new loggers from it globally visible throughout the implementation of the library. IMO this is a small requirement to place on a library to be a good citizen of the ecosystem and libraries unwilling to comply should be prepared to lose users.

The only alternative is to impose global logging facilities upon everyone using the logging API (as you have in the current proposal). It's obviously my subjective opinion, but I don't think that's a reasonable position to take for an API that intends to become a widely used standard. It's an enormous compromise in semantics compared to the relatively minor initialization / configuration requirement placed on libraries by the approach I mention above.

All I am suggestion is that you provide that facility as the baseline. Applications that wish to provide global logging facilities can do things like this:

let logger = Logging.make("global logger", using: MyLogHandler())

or this:

private let rootLogger = Logging.make("root logger", using: MyLogHandler())
extension Logging {
    static func make(_ label: String, metadata: Logging.Metadata) -> Logger {
        return rootLogger.make(label, metadata)
    }
}

Libraries can do something like this:

private(set) var logger = Logging.make("global logger", using: NullHandler())
// may not be a top-level function, this is just an example
func configureLibrary(logger injectedLogger: Logger) {
    logger = injectedLogger
}

Or use a similar technique as above to provide a global logger factory but not a global logger.

I don't know how compelling it is either in common usage, but I think it is something that should be supported by an API that desires to become a common currency logging API that is able to meet everyone's needs. IMO this is the most flexible / general approach and it also avoids the undesirable semantics of a global logging back-end that could be reconfigured dynamically (unless you plan to trap if bootstrap is called more than once?), rather than initialized only once during application startup.

Does this really come down to not being willing to require libraries to support an injectable logger? If so, why is it acceptable to bend over backwards to meet the demands of these libraries to not support injectable logging while still interoperating with the ecosystem standard logging facilities?

I don't have time at the moment but will try to come back to this tomorrow.

I definitely would not recommend a DI framework. Is it really such a tall ask for applications to perform initialization of the libraries they use? You are already requiring them to call Logging.bootstrap so they are already required to do something. I imagine many other libraries also require some kind of configuration / initialization anyway as well. The only difference is that applications must provide a logger when initializing the libraries. If this is too much to ask then it is an extremely sad commentary on the state of our profession.

I am trying to make the case that you have a better chance of widespread adoption if you have clean semantics. People might grumble about a few lines of initialization code, but I don't think too many people will say "I'm not going to use the ecosystem standard logging API because it requires a few lines of initialization code". On the other hand, people may very reasonably choose to say "I'm going to try to avoid the ecosystem standard logging API because it doesn't provide clean semantics".

Value semantics is not really a good fit for loggers. The logger can provide value semantics for metadata, but the entire point of your design is that loggers will all share the same handler back-end. This inherently involves reference semantics.

A design that embraced value semantics would require the log, debug, etc methods to be mutating. Loggers would need to be passed inout from the top level of the application and could therefore not be stored outside the call stack (in properties, etc). A design like this is clearly a non-starter so I don't think we should pretend loggers have value semantics of any kind.

That is why I prefer to explicitly support deriving a logger using a method that makes it clear that the derived logger carries with it an immutable copy of the metadata contained by the initial logger as well as any additional metadata provided when the new logger was derived. With this design it is clear that loggers have reference semantics, but the metadata is immutable. The only operation on metadata is to add additional contextual metadata by deriving a new logger.

I'm curious to hear your thoughts on what I wrote above. In general I love value semantics, it is one of the best things about Swift! However, I don't think it's a good fit for an API like this. IMO the look and feel is deceptive and not aligned with the semantics.

Can you provide an example of a handler (other than a no-op handler) with value semantics? Isn't the entire point of handlers to perform side effects of one kind or another to capture the log entries?

That may be, but I don't think it's reasonable to expose that capability directly on the logger. By making this facility visible on the ecosystem standard logger API you are endorsing this behavior. If people want to do something dirty like set the application-wide log level from some random code there are plenty of other ways to do it. We want to encourage good practices not bad ones, don't we?

+1. IMO we should require pretty significant motivation to diverge and I'm not convinced such motivation exists. "Different teams are working on it" is definitely not sufficient motivation.


(Cory Benfield) #39

I'd argue that in some cases, yes, it's really a tall ask.

Library initialisation is not a trivial thing to build. You have to solve the following three questions:

  1. How should my library behave if it is used before being initialised?
  2. How should my library behave if it is initialised multiple times?
  3. What happens if my library evolves and wants new initialisation steps: how do I evolve the API?

These questions don't have trivial answers, and if you think back on the best libraries you've worked with in your career and ask "did these libraries have an initialisation step", you are likely to find that the answer is "no they did not". Library initialisation is a common source of bugs and programmer friction, and if it can be avoided it should be.


(Neal Lester) #40

It has already been conceded that providing a canonical globally accessible Logger factory is a compromise. One specific issue is that explicit initialization / configuration of logging in libraries forces application developers to initialize their logging system before other libraries. With the proposed design it is possible for application developers to touch a subsystem before initializing the global logging factory potentially missing important messages from that subsystem. I do not see how that issue as so "enormous" that would induce application developers to avoid using the proposed logging library. If they are smart enough to understand the pitfall they are smart enough to step around it. Would you summarize any other specific technical problems introduced by that design which you think make the compromise "enormous".

In reality this doesn't always happen correctly. For example: You need to add logging to debug code which didn't require logging before and thus doesn't have access to a logger. The refactoring necessary to get a logger to the code introduces the potential for adding bugs to already buggy code (and also takes significantly longer). For an application in production, it might be better to add a logger from "thin air", use the resulting information to debug and fix the issue, and then possibly refactor to properly get a logger to that previously obscure corner of the code.

Also, explicit configuration forces an application developer who doesn't care at all about logging to deal with it anyway. With the proposed solution they can ignore logging altogether with the only cost being some console output from their libraries.

If dynamic reconfiguration isn't supported, ignore and log would be better.


(Matthew Johnson) #41

I agree that allowing uninitialized use as well as multiple initialization are both problematic designs. The design of this logging API is vulnerable to exactly these problems and they are part of the reason I am arguing against this design!

These problems arise when a library exposes globally visible APIs rather than requiring explicit initialization of instances and it also requires initialization of some kind . The best libraries I have used avoid this kind of global state and use explicit instantiation instead before making their APIs available. This avoids the possibility of use before initialization and also avoids multiple initializations applicable to the same instance. This is the kind of design I am advocating for here.

I haven't seen your questions answered for the proposed logging API. How should it behave if someone requests a logger before bootstrap is called. Is it a trap or do they get a no-op logger or a logger with some default handler? What if bootstrap is called more than once? Does the application's handler get overwritten and every subsequent logger that is created get routed to the "wrong" handler (i.e. not the one the application bootstrapped with)?

My argument is precisely that we should not adopt a problematic design for this API because we think it will avoid trouble for other libraries. We should adopt a well-designed API with clean semantics and hope that other libraries doing the same will win out in our ecosystem.

For starters, see the questions Cory asked above which to my knowledge have not been answered for this library and particularly with regard to #2, I don't believe a satisfactory answer exists. As far as I can tell, as designed a library has the ability to stomp on an application's handler (as well as arbitrary application code that has no business messing with the handler). The same issues apply to mutating metadata, setting the log level, etc.

Can people ship functioning applications using designs that have semantics like this? Of course they can. Does that mean it's a good idea to design APIs with semantics like this? No it does not.

I am not arguing against the ability for applications to do things like this! Using the approach I have suggested, an application could trivially support a global makeDebugLogger() function. All I am arguing against is the idea that we should impose this capability on all code that uses this logging API.

Are you really suggesting that we should make important semantic design choices of an ecosystem standard logging API based on the stated indifference about logging of an application developer "who doesn't care at all about logging"? Sure, they might grumble about having to pass a logger to a library but people grumble about all kinds of things and then move on.

My point is that there is no good design choice that can be made here. If you support dynamic reconfiguration then the application's handler can get overwritten. If you don't, code that expects to specify a handler is broken.


(Cory Benfield) #42

Both of these have multiple perfectly reasonable valid answers (I'm not going into them here, happy to do so off thread if you'd like to follow-up on it). The advantage with this proposal is that there is only one place that needs to answer them. Failure to initialise should get a default behaviour: what, exactly, that default behaviour is can be decided by this working group. Multiple initialisation should be reasonably straightforward to avoid by way of noting that the only place that should ever call bootstrap is whichever Swift module owns main.swift, but if it is not avoided we can choose any failure mode we believe to be reasonable, up to and including trapping.

In the multiple initialisation case, however, the likelihood of this happening is almost zero. It would require that a library choose to call bootstrap, which they should not be doing. Your proposal does not prevent this either: in a hypothetical proposal where there is no global logger factory, libraries should not be manufacturing their own isolated logging subsystems, but nothing prevents them from doing so if they want to. This API discourages that behaviour by making it extremely straightforward to get hold of a logger.

I personally strongly recommend that libraries be willing to be passed loggers, rather than magicking them out of thin air. However in my view there is simply too much prior art in favour of "magic loggers that appear from nowhere due to the magic of mutable global state" to decide to ditch that, even when as purists we would prefer to.


(Matthew Johnson) #43

How is this industry ever going to move forward if this is the attitude adopted when defining a standard API for a very young ecosystem? Are we supposed to be forever wedded to that prior art which many of us agree is not a great idea?


(Neal Lester) #44

It would be straightforward in application code (by recreating from scratch a facility this proposal provides), but wouldn't it be a bigger headache in library code (I mean when debugging library code used in an application)?


(Cory Benfield) #45

By picking our battles.

When designing this standard API we need to wrestle with the fact that, while the ecosystem is young, it has running code already in production. Abandoning those early adopters or forcing them to change is sometimes valuable, when that change unlocks important new abilities for them, but in this instance I don't believe it does. The downside of taking such a hard-line approach is that it fails

Logging is simply a library, it costs nothing more than code to build and use an alternative, and most of the ecosystem has already built their way of doing it. If we don't support them, they will simply continue to use their own model, and this new API has failed before it started.

Worse, the "injected logger" approach is trivial to support when using it from "magic global logger" frameworks and libraries, as they can simply wrap their magic global logger in an instance logger. This means that users adopting the new API that want to use "magic global logger" libraries just get no logging (or worse, two incompatible logging infrastructures), while users keeping their "magic global logger" can trivially also use libraries and frameworks with the new API at full functionality. This hardly provides an incentive to change your code!

I think there is a time and a place for refusing to support disfavoured ways of doing things, but in my view the tradeoffs here don't support that course of action. Of course, reasonable people may disagree. :slightly_smiling_face:


(Matthew Johnson) #46

There are a quite a few approaches to meeting the desire for configuring different log levels for different parts of the codebase without allowing the global level to be set from anywhere. They all rely on using the derived logger approach rather than mutating an existing logger.

The smallest change to the current design would be to simply allow a new log level to be set when deriving a new logger, although that gives up all upstream control over the log level. A small enhancement to this approach would be to allow the application to specify a minimum and maximum level when configuring the root logger (and wiring up the handler). When a new logger is derived from an existing logger, an optional log level could be requested (perhaps along with a new requested minimum and maximum). These values would be clamped to the min and max of the existing logger.

The downside of this approach is that the derived logger may not actually get the level it asks for, but that behavior would be made clear by the names used in the API. The upside of this approach is that an application still has the ability to control log levels in a centralized manner if desired (just set min and max to the same value) or to provide a bound on the configurability of the log level. Applications that don't care would just not specify the min and max log levels.

Another approach would be to make Logger generic over a phantom Capabilities type. Using this approach, you would be able to use constrained extensions to provide different APIs depending on the specified capabilities. This approach introduces some complexity, but would scale up to support lots of different behaviors (and absence of behaviors!) allowing the API to adapt itself to more usage scenarios. For example, you could also make the ability to set thread local metadata a capability. Typealiases could be used for common configurations so if it desirable.

A simpler, but more rigid approach than making Logger generic would be to just hard-code a second concrete logger type that supports the ability to specify the log level, leaving the "basic logger" without that capability and only supporting it on an opt-in basis by user the LoggerSupportingDynamicLevels (or whatever you want to call it). The premise of this approach would be that changing the level is the only capability you really care about differentiating and that most of the time you don't want code to do that, but do need to do it in a few places.

Another approach I have seen used in some places is to specify a "context" alongside the log level when emitting messages and support setting a log level for each "context" at the root. This approach would require the ability to optionally map the context when deriving new loggers. It's probably not the right approach for an API like this, but I mention it as another point in the design space that I have seen people use.


(Matthew Johnson) #47

You're right that it could be, depending on the design of the library. But how often would this actually be needed in a shipping version of a library?

Absolutely! I am taking the long-term view here.

One option we haven't discussed is layering the API so that the baseline functionality has clean semantics, but people can also choose to import GlobalLogging. Libraries could choose whether they require users to bootstrap the global logging facility or whether they use an injected logger. This would make the tradeoffs explicit and applications wishing to avoid this could seek out libraries that do not rely on it.