opentelemetry-go
opentelemetry-go copied to clipboard
Support slog for internal logs, beside of logr
Problem Statement
The log/slog
package already released in Go 1.21. But it's unable to set for otel.SetLogger
, because it expects logr.Logger
.
Proposed Solution
OTEL uses only a few log functions (Error
, Warn
, Info
, Debug
), so a new interface can be defined, for example:
type LogAdapterer interface {
Info(msg string, keysAndValues ...interface{})
Error(err error, msg string, keysAndValues ...interface{})
Debug(msg string, keysAndValues ...interface{})
Warn(msg string, keysAndValues ...interface{})
}
In order to keep backward compatibility, a new function should be introduced, which sets this kind of logger, for example:
func SetLoggerAdapter(logger LogAdapterer)
The original otel.SetLogger
is kept for backward compatibility (but marked as deprecated), which calls otel.SetLoggerAdapter
with the exported logr
adapter.
The Error
, Warn
, Info
and Debug
functions are kept in internal/global
package, but it calls the configured logger adapter, so other code part should not be changed.
A new adapter should be created for log/slog
package, which can be passed to otel.SetLoggerAdapter
.
Alternatives
Another alternatives don't keep backward compatibility.
Prior Art
N/A
Additional Context
N/A
If you don't mind, I can make a pull request for it.
Why not just wrap the slog.Logger
into a logr.Logger
?
This seems like it will bloat the API. Given we still need to support Go 1.20 I would like to think this through before we add and deprecate something in a stable API.
https://github.com/go-logr/logr/issues/171#issuecomment-1694728621
logr.Logger
is not an interface but a struct and so you can't pass an alternate implementation.
It would make more sense for OTel to define its own logger interface that it uses (that can match the logr.Logger
methods) so that we can wrap whatever logger we want in that interface and provide it to the library.
See https://github.com/go-logr/logr/blob/master/slogr/example/main.go to check how to plug a slog handler as logr logger.
More: https://pkg.go.dev/github.com/go-logr/logr
Yeah, but this is forcing anyone using the opentelemetry package into taking a dependency on logr
and creating a logr
implementation.
What most libraries do is define an interface that they expect - see this for example: https://github.com/temporalio/sdk-go/blob/master/log/logger.go#L29-L34
Correct, we decided to use logr
as the logging abstraction instead of creating our own. logr
was created for this purpose.
slog
was not available when we were building OTel internal logs, but https://pkg.go.dev/github.com/go-logr/logr/slogr offers interoperability.
More: https://github.com/go-logr/logr#background
slog
is supported via https://pkg.go.dev/github.com/go-logr/logr/slogr. We may want to describe it somewhere in the documentation. E.g. here: https://pkg.go.dev/go.opentelemetry.io/otel#SetLogger. We could add an example once we drop support for Go 1.20.
Ok, that makes sense - I was not particularly interested in slog
myself, but just a more generic interface that didn't require taking on another dependency on a different logging library.
I see that logr
is relatively light weight - I was just surprised that we needed to take a dependency on it to provide the 4 methods that are actually needed for the logger in OTel.
Using slog as a logr backend (logr.LogSink) is a good tradeoff nowadays.
For long term, the app and library developers must choose between the 2 logging interfaces: logr vs slog. I cannot predict which will be the winner after 2 years. I'm using Go for 6 years and the famous log library was changed in each 2 years.
I accept @MrAlias opinion, the API should not deprecate a call, because it introduces a long-term confusing situation. So, if the app which is using an slog-based logging solution (the backend of slog can be zerolog or logrus), the https://pkg.go.dev/github.com/go-logr/logr/slogr can be used to set custom logr logger as internal logger of OpenTelemetry library. So the call chain can be, for example: OpenTelemetry --> logr --> slog --> logrus.
I don't know exactly, what is the learning lesson of this story. Maybe if the Go std lib developers had introduced the slog earlier (or they had integrated logr), we wouldn't be in this situation now.
Maybe if the Go std lib developers had introduced the slog earlier (or they had integrated logr), we wouldn't be in this situation now.
This is correct. We added the internal logger in https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.3.0 (released in Dec 10, 2021). The slog proposal https://github.com/golang/go/issues/56345 was created in Oct 20, 2022.
Side note: OTel was driving one of the slog design decisions: https://go.googlesource.com/proposal/+/master/design/56345-structured-logging.md#levels
I have an issue with using logr. Using go-logr/zapr
to wrap our Uber Zap logger, it prints / gives incorrect log levels. This is because otel uses 0, 1, 4, and 8 as log levels, this causes negative levels within Zap from the logr to Zap translator causing nothing to get logged.
As a result, I have to have otel -> logr -> zapr -> custom zap.Core -> zap, or otel -> custom logr -> zap. If this used an interface with the appropriate methods, I could have a simple shim without needing zapr anywhere and leave the leveling up to the given users and their impls.
I have an issue with using logr. Using
go-logr/zapr
to wrap our Uber Zap logger, it prints / gives incorrect log levels. This is because otel uses 0, 1, 4, and 8 as log levels, this causes negative levels within Zap from the logr to Zap translator causing nothing to get logged.
This is a known thing. See: https://github.com/go-logr/zapr#increasing-verbosity
As a result, I have to have otel -> logr -> zapr -> custom zap.Core -> zap, or otel -> custom logr -> zap. If this used an interface with the appropriate methods, I could have a simple shim without needing zapr anywhere and leave the leveling up to the given users and their impls.
You can also write a custom logr.LogSink
. The interface wouldn't be probably much different. Take notice that even https://pkg.go.dev/log/slog#Level uses similar logging levels.
Thanks to logr
we give the user the flexibly too choose between existing logr sinks or creating your own. What would be the difference between otel -> custom otel log adapter -> zap and otel -> custom logr -> zap?
What would be the difference between otel -> custom otel log adapter -> zap and otel -> custom logr -> zap?
Having interfaces, one does not have to deal with translating levels. Instead, it is more obvious what each function does, and keeps the deps cleaner.
I ended up writing a custom logr.LogSink
, but then I had to code dive to find the levels being used. Also, nervous about otel-go changing the levels in the future (unlikely, but possible).
I had to code dive to find the levels being used. Also, nervous about otel-go changing the levels in the future (unlikely, but possible).
I think we should document the logging levels in use in https://pkg.go.dev/go.opentelemetry.io/otel#SetLogger