Hey Johannes, Tom, everyone,
great work overall here! I think this is overall very good and not much changes are needed. Good minimal baseline of APIs that I hope we'll be able to agree on and ship :-)
I have spent a significant today (and some time ad hoc before that) on making sure that potential use-cases are well served by this form of the APIs and over all I think we're good here. I specifically focused my attention on the metadata aspects and initializing/obtaining loggers, and how it would work with libraries – I think that's the more tricky parts as well.
So overall I think the proposal is quite solid, wanted to structure the feedback some more though, so here we go:
Overall types:
The separation between Logging
, Logger
and LogHandler
is nice and clear, I like it. Implementing proxy of fan-out LogHandlers
is possible and should work just fine.
Loglevels
Since this seems to have been brought up a few times in the thread wanted to share my perspective as well. I think we're good as is, and having the levels available as an enum is very useful for uniformity of logging across libraries.
Very good to include trace, as it's a very useful level for library authors that want to include a lot of logging however it would be "too much" for debug level.
Custom log levels: I do not see a need for custom log levels "as custom log levels", however the use case can be solved by metadata or configuration on log handlers, where one would add "component: database-stuff" and have a LogHandler
that inspects metadata and e.g. does not log the database-stuff; No need to complicate the log levels. This is also exemplified by the GlobalLoggerTest::test3
Tom mentions.
Metadata
Getting the bikeshed out of the room: love the use of Metadata
rather than MDC
, and I'm coming from an MDC-heavy ecosystem 
I have verified that common Metadata / MDC patterns are fine to implement using this API, e.g. one typical pattern being that some metadata is bound to the logger's "context" or lifetime, however some metadata is added per each invocation of some user handler adding (and potentially overriding) the life-long data. After such invocation the Metadata is restored to the previous state. I see this pattern as very very common, and I think we're good here thanks to exposing the metadata
setter, example:
let perContextMetadata: LoggingMetadata = context.log.metadata ?? [:]
let data = Data() // we got it from somewhere
// naively merging things
let moreMetadata = extractMeta(from: data, withBase: perContextMetadata)
var allMetadata: LoggingMetadata = perContextMetadata
allMetadata.merge(moreMetadata, uniquingKeysWith: { (l, r) in r })
context.log.metadata = allMetadata
defer { context.log.metadata = perContextMetadata }
userCallback(&context, data)
So the above snippet gives all the right semantics, a "long lasting" metadata container can be overriden by a more specific one, and at the same time we handle the "reset to previous state" quite well – thanks to allowing the set
on the metadata itself.
Metadata: temporary metadata
The just mentioned by @artemredkin addition for per log statement metadata may be fine, however it may be worth adding this as functionality instead as a scope: .withMetadata(@autoclosure ...) { within this block that metadata is added }
instead.
This allows for nested calls within such block to work properly – in addition to just "the calls I know about". This shows up again in tracing examples where sub calls should expose the same "request id" or similar as as the encapsulating call.
Metadata: signature discussion: var metadata: LoggingMetadata?
It is not stated explicitly, but after stating at the API for a while I realized that the reason to allow nil
is to support the metadata = nil
as a way of clearing the underlying data. I "get it", but it took me a (longer) moment to realize this.
At the same time we end up paying all the "is it nil or is it just empty" checks.
I would propose skipping the Optional
wrapper on this type signature, and ask implementations which do not handle metadata to use = LoggingMetadata()
(which is exactly [:]
) as the "nil object" instead. This allows us to avoid the creeping up of such code patterns: self._metadata![metadataKey] = newValue
(multiple !
and ?
found in the examples), as well as makes general interactions simpler I believe, no need to differentiate the "nil" from "no metadata" IMHO.
Metadata: type signature, do not force String values
This has already have had some input in the thread ( and has a ticket decide type for metadata value · Issue #5 · weissi/swift-server-logging-api-proposal · GitHub ), but as it is the one "biggie" I have about the current proposal I wanted to shine some more light onto it with a specific use case, so here we go:
I also made a PR to showcase the specific type of situation I have in mind when I talk about this change, see here: [DO NOT MERGE] Exploring slight changes to APIs by ktoso · Pull Request #6 · weissi/swift-server-logging-api-proposal · GitHub
More details are in the PR, however the main point is the following:
I really would want to avoid forcing the metadata values to be String
, because "rendering" the string representation of some metadata may be "wasteful", especially if the metadata is some binary structure, that in order to be printed has to be formatted a little bit, e.g. like a traceId
. (Please note that tracing and logging are quite separate things, however the one place where they do overlap is wanting to log a traceId in all log statements within such trace's execution. I've seen this in action in a number of systems, though here's a nice discussion as well with Nike describing their use of this pattern with Zipkin: Question: Can zipkin be used to replace logging? · Issue #1453 · openzipkin/zipkin · GitHub ).
Consider the following situation, which we'd like to avoid:
- we have some library code setting up metadata in the logger before invoking the user code
- user code MAY log things, so we need to offer it the trace ID as well,
- if we force values to be String, then we cannot offer our
TraceContext
as a value for "trace"
metadata key, and we are forced to render it before invoking user core, we don't know if it will be used or not.
- consider this rendering to be "expensive enough that I care about not doing it if I don't have to"
- user code is invoked, does not log anything – thus our rendering of the metadata was in vain.
In contrast, allowing some other type as value that is "lazy in rendering the String representation" (I don't really care all that much which type we pick for this... I would be fine with Any
actually, since the purpose is only for "print it"), the execution is as follows:
- library code sets
"trace": trace
, trace being the "optimal native" representation of the trace (a bunch of integers)
- user code runs, does not log
- we never invoke rendering of any metadata objects as they were never used. Yay, we did not waste cycles.
The reason this is important is that we DO want to have many debug logging statements around for the time when we might need them. However we most likely don't have them print / turned on all the time. We do so because of the a) noise they generate and b) because it takes time to print/render things, which impacts latencies. However if we do not delay rendering of metadata... then regardless of what loglevel we set, we would always incurr the pentalty for rendering the not used metadata 
TL;DR; The metadata API should allow either Any
as value (which in quick silly benchmarking I did not find to be a performance issue hmm...), or some other type that "delays the rendering", be it CustomStringConvertible
, some LoggingStringConvertible
or anything else the community decides upon.
Minor naming bikesheds...
Minor things, feel free to ignore, or ponder about:
-
Logging.make("hello.world")
– Not in love with the "make" in the name... reads very weird. I don't "make a logger", neither do I "make the hello.world
". I understand the need for this rather than a plain init
since the dance with the lock, but perhaps we can work around it somehow... Logging("...")
or Logging.get
if we need the method would read nicer. Anyway, minor thing, does not affect usefulness of the API.
- freezing and/or intercepting
bootstrap()
calls is somewhat very tempting to myself and my use cases. But I like going forward with a minimal subset and only later on figuring out the additions here. (This is a thing that could be added later on on Logging if we decide it would help). I think we're fine to start with "recommendations" in documentations and if we see there is a lot of problems here then we can figure something out. Only instance where I can imagine issues is where 2 "runtimes" / "frameworks" collide by wanting to take over the Logging.make
capabilities.
Having that said... the overall API looks great, and I'd like to polish the Metadata part a little bit and it would, in-my-eyes, seem to be ready to go. We don't aim to solve all things, but get a "common ground" with this API after all 
Hope this helps!