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

proposal: log/slog: return Level from Logger or TextHandler #60936

Closed
ghost opened this issue Jun 22, 2023 · 10 comments
Closed

proposal: log/slog: return Level from Logger or TextHandler #60936

ghost opened this issue Jun 22, 2023 · 10 comments
Labels
Milestone

Comments

@ghost
Copy link

ghost commented Jun 22, 2023

slog.HandlerOptions has a field Level which sets the minimum level that will be logged. this value is stuffed inside a slog.TextHandler, which is in turn stuffed inside a slog.Logger:

https://pkg.go.dev/log/slog#example-HandlerOptions-CustomLevels

however it seems the Level cannot currently be returned from slog.TextHandler:

https://pkg.go.dev/log/slog#TextHandler

or slog.Logger:

https://pkg.go.dev/log/slog#Logger

is this an oversight, or is the ability to return the current level from these types intentionally not available for some reason? or did I overlook some method to get that information?

@ghost ghost added the Proposal label Jun 22, 2023
@seankhliao
Copy link
Member

I believe the intention is to use Enabled to test if it's enabled at a given level or above

@ghost
Copy link
Author

ghost commented Jun 22, 2023

doesn't that remove agency from the end user? I guess you could do a loop and test Enabled for each value, but Level is an int, so the value could technically be any int value, so that seems really slow to test.

@seankhliao
Copy link
Member

what's the use case for it?
the common pattern we see is essentially

if logger.Enabled(debug) {
// expensive computation
logger.Debug(...)
}

@ghost
Copy link
Author

ghost commented Jun 22, 2023

If I have a certain struct, I would like to print this struct in different formats depending on the user preference. so user could choose something like:

0: print nothing
1: print one line
2: print popular fields
3: print all fields

I originally did something with fmt.Formatter, but it seems like slog might be a better option. however for slog to work, I need to be able to return the level of an arbitrary slog.Logger, either the default logger or some custom one. its unfortunate because I know the data is there, otherwise Enabled wouldn't exist. but the current API is so restrictive as to basically make my use case impossible to do with slog.

@seankhliao
Copy link
Member

I don't see why your usecase can't work with Enabled,

if logger.Enabled(debug) {
    // 3
} else if logger.Enabled(info) {
    // 2
} else if logger.Enabled(warn) {
    // 1
...

and with Enabled, you get more consistent output it your users pass in intermediate levels like debug+2

@seankhliao seankhliao changed the title log/slog: return Level from Logger or TextHandler proposal: log/slog: return Level from Logger or TextHandler Jun 22, 2023
@gopherbot gopherbot added this to the Proposal milestone Jun 22, 2023
@apparentlymart
Copy link

apparentlymart commented Jun 22, 2023

FWIW, I would find it quite surprising if changing the log level caused the content of one particular message to change, rather than just deciding whether or not each message is included at all.

Of course I don't know the details of your application so perhaps yours is a valid exception for an unusual situation, but it does seem like an odd behavior to encourage by exposing it directly in the standard library. The current API design encourages robust implementation of the common case of using the levels only to filter out entire log messages below a certain watermark.

If this were accepted, perhaps it would warrant a note in its doc comment explaining that logger.Enabled is the better thing to use for the more common situation of just filtering the log lines, to try to discourage code that would lead to confusing behavior if someone is using intermediate levels.

@ianlancetaylor
Copy link
Contributor

CC @jba

@apparentlymart
Copy link

apparentlymart commented Jun 23, 2023

@4cq2 I expect I would design that such that the top-line information like the method and URL gets written out as an "info" message, and then the full request header and body gets separately written out as another "trace" message, so that if I'm using info-level logging I see only the first message but can enable trace to see the other.

With that said, I'm not meaning to suggest that what you want to do is wrong, only that I would find it surprising when I bring my experience with other software. Specifically, a common thing I'll do when debugging is to first log at a relatively high level to get a broad overview of what's going on, and then gradually get more granular until I find the information I need.

In this process I rely on being able to find the same log lines I saw on previous runs as an anchor to reduce the number of more-verbose log lines I need to read through. If the messages I saw before were suddenly different when I selected a more granular level, I expect I'd be skeptical about whether I'm still observing the same behavior I was observing on the previous run.

This is a subjective thing and I expect others work with application logs differently, so I'm not meaning that my perspective is universal. If others have different expectations than I do then I've no problem with that.

@ghost
Copy link
Author

ghost commented Jun 23, 2023

I expect I would design that such that the top-line information like the method and URL gets written out as an "info" message, and then the full request header and body gets separately written out as another "trace" message, so that if I'm using info-level logging I see only the first message but can enable trace to see the other.

@apparentlymart thats a good idea. I think I was avoiding it, because it would result in something like this:

slog.Log(0, "basic stuff")
slog.Log(1, "extra info")
slog.Log(2, "moooooooooooooooore info")

instead of just a single Log call. I realized that the log constants are only using the first 7 bits:

https://godocs.io/log#pkg-constants

which means the other (32-7) or (64-7) bits are available (for now) to sneak some state in. so I came up with the below ugly code, which might do the job. I guess this issue can probably be closed, I will leave it up for another day in case someone thinks its worth pursuing.

package main

import "log"

type hello struct{}

func (hello) String() string {
   switch log.Flags() >> 9 {
   case 1:
      return "basic stuff"
   case 2:
      return "extra info"
   case 3:
      return "moooooooooooooooooooooooooooooooooooore info"
   }
   return ""
}

func main() {
   log.SetFlags(1<<9)
   log.Print(hello{})
   log.SetFlags(2<<9)
   log.Print(hello{})
   log.SetFlags(3<<9)
   log.Print(hello{})
}

@jba
Copy link
Contributor

jba commented Jun 23, 2023

@4cq2, I don't have much to add to the other answers here, but I can say that we deliberately do not have something like Logger.Level or Handler.Level (despite several requests) because we wanted to stick with the more general Enabled.

If you control the application, you can wrap your handlers in a custom handler that knows its level, like the example LevelHandler. Just add a Level method to that which returns the level.

@ghost ghost closed this as completed Jun 25, 2023
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants