Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

log/slog: structured, leveled logging #56345

Open
jba opened this issue Oct 20, 2022 · 707 comments
Open

log/slog: structured, leveled logging #56345

jba opened this issue Oct 20, 2022 · 707 comments

Comments

@jba
Copy link
Contributor

jba commented Oct 20, 2022

We propose a new package providing structured logging with levels. Structured logging adds key-value pairs to a human-readable output message to enable fast, accurate processing of large amounts of log data.

See the design doc for details.

@jba jba added the Proposal label Oct 20, 2022
@gopherbot gopherbot added this to the Proposal milestone Oct 20, 2022
@jba

This comment has been hidden.

@fsouza

This comment has been hidden.

@jba

This comment has been hidden.

@mpx
Copy link
Contributor

mpx commented Oct 22, 2022

This is a huge API surface without any real production testing (AIUI). Perhaps it might be better to land it under golang.org/x for some time? Eg, like context, xerrors changes.

@seankhliao
Copy link
Member

It's available under golang.org/x/exp/slog

@mpx

This comment has been hidden.

@deefdragon
Copy link

I love most of what this does, but I don't support its addition as it stands. Specifically, I have issues with the option to use inline key-value pairs in the log calls. I believe the attributes system alone is fine. Logging does not need the breakage that key-value args like that allow.

The complexity in the documentation around Log should be a warning sign.

...
The attribute arguments are processed as follows:

  • If an argument is an Attr, it is used as is.
  • If an argument is a string and this is not the last argument, the following argument is treated as the value and the two are combined into an Attr.
  • Otherwise, the argument is treated as a value with key "!BADKEY".

The suggestion was that potential problems with key-value misalignment will all be solved by vet checks. As I mentioned in this thread of the discussion, relying on vet should be viewed a warning as to potential problems with the design, not a part of the design itself. Vet should not be a catch-all, and we should do what we can to avoid requiring vet warnings to safely develop go.

An accidentally deleted/missed or added/extra argument in key value logging would offset the keys and values after it. That could easily bog down a logging system trying to index all the new "keys" it is getting. It could also lead to data being exposed in a way that it should not be.

I acknowledge that neither of these examples are all that likely in a well defined system, or somewhere with good practices around code reviewing etc.. But they are possible.

@hherman1
Copy link

@deefdragon Doesn't this concern apply to Printf as well? Is the difference the dependency on these logs by external systems..?

@prochac
Copy link

prochac commented Oct 24, 2022

Based on that the Go standard library is very often being recommended as source of idiomatic code, and this package aspires to be merged as part of it, I would like you explain to me the involvement of context package.

If some object uses logger, isn't it its dependency? Shouldn't we make the dependencies explicit? Isn't this logger smuggling bad practice? If passing the logger by context is idiomatic, is *sql.DB too?

Why has the logger stored context? It violates the most famous sentence from the documentation for context

Contexts should not be stored inside a struct type, but instead passed to each function that needs it.

Logger in context containing its own context inside ...

Frankly, I'm a bit confused.

@deefdragon
Copy link

@hherman1 The same concern does apply to printf, tho it's not as bad compared to logging. With printf, most statements are consumed as a single chunk, and only consumed locally by the programmer. Being misaligned is easy enough for a human to notice, parse, and correct.

In the case of Sprintf, where it might not be consumed by the programmer, and instead be used as an argument to something, the "testing" during development that is making sure the program starts would likely catch most misalignment.

Being off by one in a log is much harder to catch as it has no real impact in the program's execution. You only notice there is an issue when you have to go through your logs.

@mminklet
Copy link

mminklet commented Oct 25, 2022

I think I share some of @prochac 's concerns regarding context. Maybe I'm being a bit of a luddite, but recommending that the logger is passed around inside context rather than via explicit dependency injection, smells a bit funny to me. Context, from what I have always followed, is for request-scoped information, rather than dependencies. And the more clarity surfacing dependencies the better. IE just assuming the right logger is in context, and getting the default one because it's still getting some logger

@v3n
Copy link

v3n commented Oct 25, 2022

Maybe I'm being a bit of a luddite, but recommending that the logger is passed around inside context rather than via explicit dependency injection, smells a bit funny to me. Context, from what I have always followed, is for request-scoped information, rather than dependencies.

I think there are two approaches here:

  • For long-lived process (think the kind of thing that you would pass context.Background() to); I would absolutely recommend dependency injected loggers. This would include situations where a context logger might be available, but may use fan-in/singleflight to demux requests. For these cases, the logged is frequently implemented as a 'child logger', which in some frameworks allows you to adjust the log level per-child.

  • However, on the other hand, many APM services have a "log correlation" features (this is even part of the spec for OpenTelemetry). In this situation, you want your context to be propagated by the logger; as the correlation fields would be prebound to the logger and propogated down the stack.

I've used both patterns frequently in high-scale production services; and both have their places. I'd definitely like to see slog promote context-propagated logging as the observability benefits are huge.

@mminklet
Copy link

mminklet commented Oct 25, 2022

Appreciate your explanation @v3n . I'm still having a slightly hard time understanding the benefit of the logger itself being passed around in context. I understand propagating the log correlation information via context, and we currently use the otelzap implementation that does this sort of thing via ErrorContext(ctx, ...) etc logging methods. I like the WithContext methods proposed here, passing the context to the logger, in similar fashion. It's more the logger itself being passed around inside the context that feels a bit odd to me

The zap and otelzap libraries do allow for the same kind of thing, whereby you can get the logger from context etc (and I'm sure others do), it's just this being in the std library it's more of a recommendation for this kind of pattern

@seankhliao
Copy link
Member

I still want a standard handler for testing.TB.Log like https://pkg.go.dev/github.com/go-logr/logr@v1.2.3/testr#New

@jba
Copy link
Contributor Author

jba commented Oct 25, 2022

Being off by one in a log is much harder to catch

@deefdragon, we'll have a vet check for that.

@jba
Copy link
Contributor Author

jba commented Oct 25, 2022

I still want a standard handler for testing.TB.Log like https://pkg.go.dev/github.com/go-logr/logr@v1.2.3/testr#New

@seankhliao, such a handler seems easy to write, and it's not clear to me yet whether there is enough demand to include it. Let's hold off for now; we can always add it later.

@jba
Copy link
Contributor Author

jba commented Oct 25, 2022

Why has the logger stored context? It violates the most famous sentence from the documentation for context

Contexts should not be stored inside a struct type, but instead passed to each function that needs it.

@prochac, that is a design principle, not a hard-and-fast rule. It is there to steer people away from buggy code, but that has to be weighed against other factors. In this case, we knew that passing tracing information to logging was an important feature, but we didn't want to add a context argument to every log output method. This was our solution.

@jba
Copy link
Contributor Author

jba commented Oct 25, 2022

Context, from what I have always followed, is for request-scoped information

@mminklet, scoping a logger to a request is a common pattern, and is probably the main application of the ability to add a Logger to a context. It doesn't preclude dependency injection; if that works for you, stick with it.

@amnonbc
Copy link

amnonbc commented Oct 25, 2022

This is a significant proposal. @jba can you do a video talk on this. And, perhaps, a blog post?

@deefdragon
Copy link

Being off by one in a log is much harder to catch

@deefdragon, we'll have a vet check for that.

@jba As I said in my original post, I don't think that's a good solution.

relying on vet should be viewed a warning as to potential problems with the design, not a part of the design itself.

@fsouza
Copy link
Contributor

fsouza commented Oct 25, 2022

I like this in general. One API nit from an experiment in s3-upload-proxy: it would be good to have a way to convert a string into to the level (say you want to allow users set an environment variable like LOG_LEVEL=debug and have that translated to DebugLevel).

Other libraries (logrus, zerolog, zap) call that function ParseLevel (for zap it's ParseAtomicLevel, but same principle).

@jba
Copy link
Contributor Author

jba commented Oct 25, 2022

can you do a video talk on this.

Done.

@AndrewHarrisSPU
Copy link

Other libraries (logrus, zerolog, zap) call that function ParseLevel

The additional '+'/'-' terms put a twist on this, I think it'd be nice to have. (I had this laying around: https://go.dev/play/p/Izwzgd8Kmc9)

@amnonbc
Copy link

amnonbc commented Oct 26, 2022

Three comments on the proposal:

One thing which irritated me with zap was the existence of both sugared and attribute log methods.
This doubled the API surface, and created a coding style balkanisation of packages that used zap as their logger.
In Go there should be one, and preferably only one way of doing things.
slog does unfortunately replicate this duplication.

My second observation is that we have 10 Attr constructors, one for each common type we want to log, + any.
Could we have used Generics to reduce the API surface here?
But if we are going to have explicit Attr constructors, then I would like one which logs an error, one that logs a stringer,
and one which logs a []byte as a string.

Finally, I think it is very good (but perhaps overdue) that we are moving towards a canonical production strength logging library in stdlib. Most libraries need some level of logging, if only for debugging. And not having a standard interface of
sufficient power meant a lot of pain. If you want to import package foo, you also need to pull in the weird and wonderful
logging library that they use, construct a logger of the right type to pass to it, and arrange for the output to somehow
be integrated into the logging output and config used by the rest of your system. I have done this myself far too many
times and it quickly gets tedious. So great that this will probably soon move into the stdlib,
and that new packages will eventually start adopting it. If only we had had it earlier, much aggravation would have been saved.

@jba
Copy link
Contributor Author

jba commented Oct 26, 2022

slog does unfortunately replicate this duplication.

I disagree. There is only one Attr-based output method, LogAttrs. It is there as an optimization for those who need it. Most users will be fine with the other five output methods. (And yes, there is also the extra LogAttrsDepth method, but that will be even more rarely used.)

Could we have used Generics to reduce the API surface here?

The answer is, "hopefully." With the current implementation, you can only reduce the API surface at a considerable time penalty. But that may change before this API is frozen. See this item in the discussion.

I would like [an Attr constructor] which logs an error, one that logs a stringer,
and one which logs a []byte as a string.

According to my analysis, zap.Stringer uses are 2% of all constructors, and zap.ByteString is at 0.3%. I don't think those numbers warrant more API. (But we can always add them later if I'm wrong.) zap.Error calls are quite common, but we believe that the error argument to Logger.Error will absorb most of those.

@rsc
Copy link
Contributor

rsc commented Oct 26, 2022

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@rabbbit
Copy link

rabbbit commented Oct 26, 2022

Why has the logger stored context? It violates the most famous sentence from the documentation for context

Contexts should not be stored inside a struct type, but instead passed to each function that needs it.

@prochac, that is a design principle, not a hard-and-fast rule. It is there to steer people away from buggy code, but that has to be weighed against other factors. In this case, we knew that passing tracing information to logging was an important feature, but we didn't want to add a context argument to every log output method. This was our solution.

I'm with @prochac and @mminklet in that passing logger in context seems awkward.

However, I see your point about context propagation and @v3n point:

I'd definitely like to see slog promote context-propagated logging as the observability benefits are huge.

But then context-propagated logging and context-propagated logg**er** are slightly different things.

@jba (apologies if this was already discussed in #54763, I tried to & failed to find it there) did you consider something like context-propagated log-context, as opposed to propagating the whole logger?

I frequently wish I could do easily is adding log-context to an already existing logger, like:

logger = slow.WithContext(ctx).With("method", r.Method ...)

.....

err := doSomething(c ctx) err {
   doSomethingElse(c ctx) {
        somethingVeryDeep(c ctx) {
             // do some other work
             slog.AddToContext(ctx, "workResult", ok)
        }
        somethingVeryDeepAgain(c ctx) {
             // do some work
             slog.AddToContext(ctx, "otherWorkResult", ok)
        }
   } ()
} ()
if err != nil {
    logger.Error("requestFailed") // `slog` extract values for me.
}

This would allow me to log once per request/error instead of tens of scattered log-lines which I then need to correlate with request/span ids.

I think this could also support https://go-review.googlesource.com/c/proposal/+/444415/5/design/56345-structured-logging.md#754

ctx, span := tracer.Start(ctx, name, opts)

since tracer.Start could add the context it needs to the log-context on the context.

This would likely require changes to the context package to support this in a performant way, but stdlib wants to support context+logging+tracing natively perhaps context can be adapted too? Maybe changes won't be necessary.

@AndrewHarrisSPU
Copy link

There may be a hint of namespace clobbring in ReplaceAttr, where matching on a key string is invoked before group prefixes are applied (example: https://go.dev/play/p/yFNXLz3gklD).

I think it's a reasonable behavior; the version of ReplaceAttr that is attentive to group prefixing doesn't seem like it would be very robust, leading to situations where it's not possible to add a Group downstream without breaking a replacement policy (or update a package because their logger added a Group, etc.). Still I wonder if it's worth noting in documentation.

A version of ReplaceAttr in Handler middleware might offer some flexibility. I really have no opinion on whether that component should be in slog - it's not very hard to implement, and eventually precisely tailoring some more elaborate replacement policies or hooks or whatever seems like it should be written outside of slog.

@jba
Copy link
Contributor Author

jba commented Mar 27, 2023

Everyone, minor changes to slog continue to be discussed in other proposals, not all of which refer back to here. If you're interested, you can search for them. Most would benefit from more eyeballs.

@telemachus
Copy link

Everyone, if you'd like to share a Handler, wrapper or other bit of slog code, feel free to add it to that page.

@jba (I apologize if this isn't the right place to ask these questions, but I didn't want to edit the wiki without asking.)

  • https://pkg.go.dev/golang.org/x/exp/slog doesn't seem to be getting further updates. Is it worth mentioning https://go.googlesource.com/go instead since that's where new updates seem to be going. People may want to follow that instead.
  • Several of the custom handlers are already out of date. (E.g., they don't provide a context where one is needed, or they use the wrong constants for levels.) This is likely inevitable given that slog is in flux. But is it worth adding a disclaimer above the links? Otherwise, I worry people will become confused or frustrated.

@jba
Copy link
Contributor Author

jba commented Mar 28, 2023

@telemachus, thanks. I updated the initial sentences of the wiki page with the status and location changes, and a disclaimer.

johanbrandhorst pushed a commit to Pryz/go that referenced this issue Mar 28, 2023
Format Group values like a []Attr, rather than a *Attr.

Also, use fmt.Append in Value.append.

Updates golang#56345.

Change-Id: I9db1a8ec47f8e99c1ac3225d78e152013116bff3
Reviewed-on: https://go-review.googlesource.com/c/go/+/479515
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
johanbrandhorst pushed a commit to Pryz/go that referenced this issue Mar 28, 2023
Comparing two Values with == is sensitive to the internal
representation of Values, and may not correspond to
equality on the Go values they represent. For example,

    StringValue("X") != StringValue(strings.ToUpper("x"))

because Go ends up doing a pointer comparison on the data
stored in the Values.

So make Values non-comparable by adding a non-comparable field.

Updates golang#56345.

Change-Id: Ieedbf454e631cda10bc6fcf470b57d3f1d2182cc
Reviewed-on: https://go-review.googlesource.com/c/go/+/479516
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
@jba
Copy link
Contributor Author

jba commented Mar 28, 2023

Hey all, over on #59292, we're considering removing LogValuer. One problem is we're not really sure how people want to use it. If you've written a LogValue method for slog, or something similar for another logging package, please add your story to that issue.

@Thiht
Copy link

Thiht commented Mar 29, 2023

Hey,
I realize I might have missed some discussions about this specific point so sorry in advance, but I can't find references about it: what happened to slog.NewContext(context.Context, *slog.Logger) context.Context and slog.FromContext(context.Context) *slog.Logger? I can find them cited in some places but they don't seem to have made it to /x/exp/slog. Is that correct? Is there any chance they would come back? Implementing it myself in a util package is easy, but a bit inconvenient to do every time.

@pohly
Copy link

pohly commented Mar 29, 2023

I can find them cited in some places but they don't seem to have made it to /x/exp/slog. Is that correct?

Yes.

Is there any chance they would come back?

It can get suggested in a new issue, but this comment suggests that it wouldn't get accepted.

I'm currently undecided whether I should ask for it or propose go-logr/logr as the "owner" of the key for storing a logger in a context. The latter approach has the advantage that with some help by wrappers, two-way compatibility between clients using the logr API and those using the slog API + logr for context access could be achieved (with some caveats).

Implementing it myself in a util package is easy, but a bit inconvenient to do every time.

It also wouldn't be interoperable with other libraries.

@Thiht
Copy link

Thiht commented Mar 29, 2023

#56345 (comment) suggests that it wouldn't get accepted

That's unfortunate. I'm not sure I agree with (or understand correctly) this sentence:

to avoid the presumption that every context contains a logger

slog.FromContext would belong to the slog package, so it can decide to fallback to the default slogger if the context key is empty or somehow invalid, which would make every context effectively "contain" a logger (or at least make a logger retrievable from any context). That seems to be the default behavior in the loggers I've used so far and I think it's a sensible default (especially since the function is the only way to access the context key). Does it have unintended side effects?

I'm currently undecided whether I should ask for it

IMHO it would be worth it. Involving a third party package feels weird and complicated, when slog is just 2 basic functions away from being "all batteries included".

@neilotoole
Copy link

slog.FromContext would belong to the slog package, so it can decide to fallback to the default slogger if the context key is empty or somehow invalid, which would make every context effectively "contain" a logger (or at least make a logger retrievable from any context). That seems to be the default behavior in the loggers I've used so far and I think it's a sensible default (especially since the function is the only way to access the context key). Does it have unintended side effects?

What I've implemented in the past is that my slog.FromContext equivalent would return a "discard logger" that devnulls the log calls. Returning a default logger would definitely be unexpected.

@pohly
Copy link

pohly commented Mar 30, 2023

to avoid the presumption that every context contains a logger

The implication of having a slog.FromContext is that for full interoperability, any reusable library that accepts a context should use that function to retrieve a logger. That's because the caller might have stored one. There's no consensus on whether this is how libraries should be written.

Returning a default logger would definitely be unexpected.

This is not different from slog.Info falling back to the global logger, so in the context of slog, returning a logger that really produces output seems consistent to me.

@Thiht
Copy link

Thiht commented Mar 30, 2023

I'd argue the consensus is that libraries should NOT log, in the current state of things. Or if they do, it's with 1) their own logger, 2) an exposed way to turn off/override their logger. I don't see that changing with slog in its current state. And I'd say it's fine, slog doesn't have to fix everything that's imperfect with Go's logging story.


This is a stretch, and I think out of scope for the current proposal, but has it been considered to implement some kind of namespacing to make slog more library friendly?
Something like this (not a proposal, just an illustration):

// In the library

var namespace = struct{}{}

func LibNameSlogNamespace() any {
	return namespace
}

// Can be used by a library, or by sections of your code that you want to namespace
slog.FromContextWithNamespace(context.Context, any) *slog.Logger
// slog.FromContextWithNamespace(ctx, MyLibNamespace)


// In the caller code 

HandlerOptions{
  Namespaces: map[any]slog.Level{
	LibNameSlogNamespace(): slog.LevelError,
  }
}

Log4j has a similar behaviour. I think this would solve a real itch with logging across the whole ecosystem.

@shaj13
Copy link

shaj13 commented Mar 30, 2023

@jba Is there any way to set default Level?
The output of the default handler is totally different from TextHandler which is more beneficial for human reads, IIRC it's worth exposing its behavior publicly without exposing its structure (to minimize the API surface) or exposing the structure like other handlers implemented by slog pkg.

The log pkg exposed API to set and configure the default logger.

  • log.SetPrefix
  • log.SetOutput
  • log.SetFlags

Perhaps slog may follow the same terminology but with a difference in API signature (avoid Set**)

i.e

func NewDefaultHandler(w io.Writer) Handler {
	return (HandlerOptions{}).NewDefaultHandler(w)
}

func (opts HandlerOptions) NewDefaultHandler(w io.Writer) Handler {
      lg := log.New(w, "", log.LstdFlags)
      return newDefaultHandler(lg.Output) 
}

@08d2
Copy link

08d2 commented Mar 30, 2023

The implication of having a slog.FromContext is that for full interoperability, any reusable library that accepts a context should use that function to retrieve a logger. That's because the caller might have stored one. There's no consensus on whether this is how libraries should be written.

The existence of slog.FromContext does not imply that reusable libraries can use that function to retrieve a valid logger. What goes in to a context is entirely the responsibility of the root application. Libraries can't make any assumptions about what is or isn't present.

@pohly
Copy link

pohly commented Mar 31, 2023

The whole purpose of a NewContext call would be to store a logger that called functions are meant to use when they receive a context. The library doesn't need to make assumptions. It checks with FromContext and then (depending on the implementation) falls back to discarding log entries or using the global default logger.

The pro and con of this is that the caller doesn't need to configure individual loggers for everything that might have relevant log output down the call chain. It's a con because one might also get undesired log output.

@jba
Copy link
Contributor Author

jba commented Mar 31, 2023

namespacing to make slog more library friendly

@Thiht, that is what Logger.WithGroup is for.

@jba
Copy link
Contributor Author

jba commented Mar 31, 2023

The logger-in-a-context discussion is worth having, but not here. If someone wants to champion it, make a proposal.

@jba
Copy link
Contributor Author

jba commented Mar 31, 2023

Is there any way to set [the default handler's level]?
it's worth exposing [the default handler's] behavior publicly without exposing its structure...

@shaj13, we think of the default handler as a bridge to the log package, just so the two can work together. It's not supposed to be visible or configurable.

Others have asked about this, so let me explain the rationale in more detail.

We wanted to make sure even the simplest uses of slog would do something reasonable. Even if your program just uses slog.Info, you will see output, it will look like the output of the log package, and it will go to the same place that you configured the log package to write to.

But if you decide to use slog instead of log, you want structured output, and for that you will want to install a handler that produces it, or at least outputs some machine-readable format. The default handler doesn't provide a guarantee of machine-readability.

At that point you should use slog.SetDefault to install a slog.Logger with the handler of your choice. Perhaps that handler prints the time, level and message first, followed by key=value pairs, like the default handler does, and allows you to configure the level too. slog does not provide such a handler, but one could propose that it should. However, it's easy enough to write on your own, especially if you use some handler-writing support package, like github.com/jba/slog/withsupport.

@lpar
Copy link

lpar commented Mar 31, 2023

The log pkg exposed API to set and configure the default logger.

* log.SetPrefix
* log.SetOutput
* log.SetFlags

Perhaps slog may follow the same terminology but with a difference in API signature (avoid Set**)

Support for output prefixes and format customization was already requested and rejected, several times I think. The solution is to write your own formatter or use a wrapped io.Writer to reproduce the functionality of log.

@lucasec
Copy link

lucasec commented Mar 31, 2023

I still strongly disagree with the argument not to provide any customization of the default logger. For example, I adopted slog because in my development environment, I just want log levels (and the ability to filter by level). In production, I want full structured JSON output, so here installing the built-in JsonHandler makes sense.

In my development environment, the default logger is a lot more readable than the TextHandler. Writing my own handler feels like overkill when all I want is to see my debug-level log messages.

@veqryn
Copy link

veqryn commented Apr 1, 2023

Will slog be in the standard library with the next version of Golang released?
Would it make sense to keep slog in golang.org/x/exp/slog for a little while longer (skip next golang release) while things are still being worked on and iterated on?

@sagikazarmark
Copy link

https://pkg.go.dev/golang.org/x/exp/slog doesn't seem to be getting further updates. Is it worth mentioning https://go.googlesource.com/go instead since that's where new updates seem to be going. People may want to follow that instead.

I wonder if it would make sense to keep the exp package in sync. It's much easier for people to use in their experiments than copying code from the Go source tree (or recompiling Go from tip after every update).

@sagikazarmark
Copy link

After creating my own clone of the package, I realize it may not be trivial though...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Accepted
Development

No branches or pull requests