opentelemetry-go icon indicating copy to clipboard operation
opentelemetry-go copied to clipboard

Support slog for internal logs, beside of logr

Open pgillich opened this issue 8 months ago • 14 comments

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

pgillich avatar Oct 22 '23 08:10 pgillich

If you don't mind, I can make a pull request for it.

pgillich avatar Oct 22 '23 08:10 pgillich

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.

MrAlias avatar Oct 22 '23 15:10 MrAlias

https://github.com/go-logr/logr/issues/171#issuecomment-1694728621

MrAlias avatar Oct 22 '23 15:10 MrAlias

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.

Tadimsky avatar Nov 03 '23 06:11 Tadimsky

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

pellared avatar Nov 03 '23 15:11 pellared

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

Tadimsky avatar Nov 03 '23 17:11 Tadimsky

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.

pellared avatar Nov 03 '23 17:11 pellared

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.

Tadimsky avatar Nov 03 '23 18:11 Tadimsky

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.

pgillich avatar Nov 03 '23 19:11 pgillich

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

pellared avatar Nov 04 '23 06:11 pellared

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.

jpopadak avatar Nov 10 '23 20:11 jpopadak

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?

pellared avatar Nov 10 '23 22:11 pellared

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).

jpopadak avatar Nov 13 '23 21:11 jpopadak

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

pellared avatar Nov 14 '23 09:11 pellared