golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
120.74k stars 17.34k forks source link

log/slog: structured, leveled logging #56345

Closed jba closed 10 months ago

jba commented 1 year ago

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.

ancientlore commented 1 year ago

@ancientlore (and everyone else), how about the following changes:

  • ReplaceAttr is passed a slice of groups as well as an Attr.
  • It is only called on "leaf" Attrs, not Groups; the slog package handles the recursive walk for you.
  • The value of the Attr is resolved, with Value.Resolve. This seems unrelated, but we need to resolve the value to determine if it's a group, so it comes for free.

The CL is at https://go.dev/cl/455155.

@jba thank you this will work nicely for the major use cases I can think of, and kudos on the pooling for efficiency. Brief comments:

Best,

Mike

jba commented 1 year ago

@ancientlore, changed the doc as you suggest. ReplaceAttr should certainly be allowed to return any Attr it wants, so fixed the implementation to call Value.Resolve on its return value.

hherman1 commented 1 year ago

Does passing a slice describing the group path imply more allocations?

jba commented 1 year ago

@hherman1: no, not typically. I use a sync.Pool for the slice, so for most programs, after a warm-up period, the slices will come from the pool.

08d2 commented 1 year ago

Is ReplaceAttr actually a core requirement?

@08d2 I think it is, because the standard keys like "msg" could otherwise not be changed. At minimum those need to be customizable, or many people would not be able to use the slog package.

It's not obvious to me why this kind of thing would ever need to be changed. Would stuff like the key used for an e.g. msg not be authoritatively provided during construction? I'm sure there are some use cases for which this is insufficient, but I can't believe those use cases prevalent enough to warrant direct accommodation in the core package made part of the stdlib, since they are easily accommodated by an e.g. decorator.

08d2 commented 1 year ago

Forgive me if this question has already been addressed, but it's not obvious to me from the design doc or the CL links I've identified. If I write some library code that logs via some logger provided by its caller, how would this proposal expect me to model that logger value in my library? Specifically, does it express a logger as a concrete type or as an interface?

My initial parse of the proposal suggests to me that package slog would expect my code to take a concrete *slog.Logger pointer? But this wouldn't make any sense — perhaps the single largest design error remaining in the stdlib is that the canonical logger is defined as a type rather than an interface — so I'm sure it's a failure of my comprehension, which I apologize for, and hope to be educated about. Thank you in advance!

ancientlore commented 1 year ago

It's not obvious to me why this kind of thing would ever need to be changed. Would stuff like the key used for an e.g. msg not be authoritatively provided during construction? I'm sure there are some use cases for which this is insufficient, but I can't believe those use cases prevalent enough to warrant direct accommodation in the core package made part of the stdlib, since they are easily accommodated by an e.g. decorator.

@08d2 I ran across my issues in a real-world test of trying to use the slog package for our existing ELK-based internal logging system.

When you use a function like slog.Error or slog.Info, there are standard parameters that you don't provide the key for - basically time, msg, level, source, and err. These have names defined as constants in the package (like slog.MessageKey). To be compatible with existing log collectors, we have to be able to customize those. For instance, we use @timestamp instead of time as the key. We can't easily change that across hundreds of services, dashboards, and alarms.

In the other cases where the caller provides the key, it isn't unreasonable to ask them to provide it correctly, but in the built-in cases ReplaceAttr lets you customize it. There are other use cases too - like expanding an error into a group that unwraps the inner errors. And not just changing keys - you could filter the value that was logged, for instance removing PII or PCI data that may accidentally have been logged. It's a powerful construct and optional to use, so if you don't need it, you don't really pay a penalty.

jba commented 1 year ago

If I write some library code that logs via some logger provided by its caller, how would this proposal expect me to model that logger value in my library?

@08d2: As a *slog.Logger.

It is a concrete type for efficiency. The interface you're looking for is Handler. Can you explain why you think using a concrete type for log.Logger is "the single largest design error remaining in the stdlib"?

ancientlore commented 1 year ago

@ancientlore, changed the doc as you suggest. ReplaceAttr should certainly be allowed to return any Attr it wants, so fixed the implementation to call Value.Resolve on its return value.

@jba thanks - this resolves everything needed for my slog proof-of-concept for our logging system, and quite nicely at that. By the way, I found a nice solution to the other issue where I proposed needing to "merge" groups. I didn't realize at the time that I could simply make an error type that is also a slog.LogValuer. I like this solution - quite powerful. Next up I guess I need to try out the context work.

Davincible commented 1 year ago

@jba

It is a concrete type for efficiency.

Efficiency as in it is measurably faster to not use an interface? Or usage efficiency?

I agree with @08d2 that the concrete type for the stdlib log package in things like an HTTP server is annoying, as you need to do hacky stuff to be able to pass in your own, non-stdlib logger. Although that problem does mostly go away with slog, as it's a far more complete logger that wouldn't quickly need replacement as you can just write your own handler to connect to different loggers.

08d2 commented 1 year ago

@jba

The interface you're looking for is Handler.

Ah! Okay. So let's say I'm writing a Google PubSub client library. My package 08pubsub has a NewClient constructor that takes a variety of parameters from its caller — functional options, config struct, who knows. One of those parameters will probably be a logger, which the Client can use to emit runtime diagnostic information in some way. More specifically, the Client will log stuff by calling methods on the logger value provided to it during construction. Or a logger extracted from a request context, or et cetera. It has no reason to know or care about how that logger is implemented, it only cares about the capabilities of the thing as expressed by its method-set. The relationship between Client and logger is behavioral, not stateful. My Client will always model dependencies, including loggers, as interfaces, not as concrete types. This is canonical Go: accept interfaces, return structs. So should it then take a logger slog.Handler?

ancientlore commented 1 year ago

One of those parameters will probably be a logger, which the Client can use to emit runtime diagnostic information in some way.

@08d2 a common approach I have seen for this is for your client package to define an interface it uses for logging. For convenience, the interface can define methods that slog.Logger already has.

https://go.dev/play/p/QErrG1Su1np

Callers can also provide a type that wraps any other logger and implements your interface, for instance if they aren't using a logger that has the exact signature of your interface's functions. In my example Error is the only function it will use. This separates your client code from any particular logger implementation.

08d2 commented 1 year ago

That's certainly possible. It is also possible to take this approach for the existing stdlib log.Logger. The power of Go interfaces on display! But I'm trying to suggest that a logger interface deserves a single canonical definition in the stdlib, to serve as an integration point for Go programs in general. Just like we have io.Reader and io.Writer, for example.

jba commented 1 year ago

It is a concrete type for efficiency.

Efficiency as in it is measurably faster to not use an interface? Or usage efficiency?

The former.

08d2 commented 1 year ago

io.Reader and io.Writer are interfaces that abstract over a lot of concrete types, and are successfully utilized in a lot of extremely high performance code. Do loggers have stricter performance requirements than Readers and Writers?

zephyrtronium commented 1 year ago

@08d2 Ignoring the cases where devirtualization applies (it almost never would for a logger), using interfaces tends to cause slower code primarily because any pointery argument to an interface method call must be heap-allocated, since escape analysis generally can't determine whether the dynamic type's implementation may retain those pointers. This does include the backing arrays for slices in the case of io.Reader and io.Writer, which is the reason io.Reader in particular takes a slice to fill rather than returning a slice: you can reuse storage so that the array is only allocated once, no matter how many times you read. It is much harder to facilitate reuse of storage for a logger. If slog.Logger were an interface, every argument to every call of any logging method would be forced to be heap-allocated.

There are more performance concerns, as well. Interface method calls are doubly indirect; method calls on concrete types can be direct, allowing the CPU instruction cache to fill ahead of time. (Although, I'm not sure how much that applies with position-independent code.) Interface method calls necessarily can't be inlined. Non-pointer values stored in interface variables have to be heap-allocated anyway, which can cause surprising performance drops. I feel like I am forgetting a few more...

In other words, it isn't that loggers "have stricter performance requirements than Readers and Writers," it's that loggers are fundamentally different in the first place. They're concerned about types and structures, not just binary blobs.

AndrewHarrisSPU commented 1 year ago

@08d2

So should it then take a logger slog.Handler?

I agree in the sense that a Handler really is the architectural abstraction, while a Logger could be called a "front-end logging discipline", if a "front-end logging discipline" is a wide method set used for concise logging code. (slog has one discipline, zerolog has chaining, others are context-first, etc.)

In this arrangement, dependency on a Logger is equivalent to dependency on a Handler ... the log.Handler() method of a Logger means that passing a Logger isn't a wrong option.

leinadao commented 1 year ago

Why not LevelDebug, LevelInfo etc. rather than DebugLevel, InfoLevel etc? This would seem more consistent with e.g. the standard library net/http constants MethodGet, MethodPost, StatusOK, StatusCreated etc. https://pkg.go.dev/net/http#pkg-constants

zephyrtronium commented 1 year ago

This seems broadly useful (though in retrospect I might have chosen a poor name, given that defer means a different thing in Go):

// Deferred delays evaluation of a function to produce a logged value until it
// is needed. This can be used to avoid expensive operations in logging calls
// that are filtered by level.
func Deferred[F ~func() T, T any](f F) slog.LogValuer { return deferred[T](f) }

// deferred is the implementation of Deferred. It could be exported itself, but
// using a function that returns a deferred instead allows the type to be
// inferred.
type deferred[T any] func() T

func (f deferred[T]) LogValue() slog.Value { return slog.AnyValue(f()) }

https://go.dev/play/p/83SflFb7mum

The recently published Google style guide for Go includes a logging example where the "good" is to check whether a verbosity level is enabled before computing an expensive string to log. This LogValuer seems like it could largely subsume that pattern, allowing the same code to efficiently handle disabled logs without substantially penalizing enabled ones. The cost would be allocating a single closure in both cases, but if that is expensive then I would expect a logging package like zerolog to be preferable anyway.

This is easy to write but widely applicable. Would it make sense to include something like this in slog itself?

08d2 commented 1 year ago

@zephyrtronium

It is much harder to facilitate reuse of storage for a logger. If slog.Logger were an interface, every argument to every call of any logging method would be forced to be heap-allocated.

I get this, and I get the categorical differences between a skinny concept like an io.Reader, compared to the relatively hefty concept of a logger. But I'm not quite sure about the relationship between a logger and storage which is implied here. An io.Reader models a capability which necessarily requires its implementers to be stateful; there is a clear concept of storage there. But a logger isn't usually, or at least doesn't need to be, stateful in this way; there is a concept of state, sure, but the logger doesn't usually own that state like an io.Reader does, it treats that state as a dependency. I guess many/most loggers actually use an io.Writer, for example, to represent that state!

And I hear your point about heap-allocation of method arguments, but I think in practice this is always going to be true, no? A ton of logged arguments are string literals, but there are also a ton of arbitrary values, themselves sometimes interfaces.

And, zooming out a little bit, interfaces are a core language concept, and are ubiquitous in Go code. They're also great! They solve a lot of problems very elegantly. But if another another core and ubiquitous concept like a logger can't use them for performance reasons, that seems like a problem.

zephyrtronium commented 1 year ago

@08d2

But I'm not quite sure about the relationship between a logger and storage which is implied here. ... And I hear your point about heap-allocation of method arguments, but I think in practice this is always going to be true, no?

What I mean by "storage" is the actual memory locations used to pass arguments to the methods. Because slog.Logger methods take ...any or ...Attr arguments, calling those methods through an interface forces the slices implementing the ... parameters to be heap-allocated. Each element of those slices also has to contain a heap-allocated object (because interfaces always store pointers, currently with the exception of one-byte integers). It is possible to preallocate and reuse those slices and even the interface boxes, but doing so takes quite a few lines of code.

With slog.Logger being a concrete type, escape analysis can determine that the slice elements don't escape and allocate the backing array on the stack instead. This means the elements – i.e. the arguments passed to the method's variadic parameter – in turn may remain eligible for stack allocation. So, no, it is not always true that method arguments must be heap-allocated. (Though I don't have the time to actually check what slog.Logger methods are in fact able to take stack-allocated pointers.)

Most other high-performance logging packages circumvent the issue by using method chains instead of variadic methods and taking a fixed set of parameter types instead of any. That design was rejected for slog because it enables use-after-free errors if you call the wrong methods.

And, zooming out a little bit, interfaces are a core language concept, and are ubiquitous in Go code. They're also great! They solve a lot of problems very elegantly. But if another another core and ubiquitous concept like a logger can't use them for performance reasons, that seems like a problem.

I don't think loggers are at all "core and ubiquitous" in the same way as interface types. I have written very few programs and packages that don't use interfaces, and exactly one that does use anything resembling structured logging. That said, I agree that it would be nice to be able to use more interfaces in performance-sensitive code. I just can't imagine any specific ways for a compiler to resolve the performance issues they tend to cause (outside of #45494 or something more drastic like borrow checking).

jba commented 1 year ago

There is now a wiki page for slog resources. If you write a handler or other slog-related project that you think would be generally useful, add a link to it there.

jba commented 1 year ago

Would it make sense to include something like this in slog itself?

@zephyrtronium, let's wait for extensions like that until we have more usage information.

jba commented 1 year ago

And I hear your point about heap-allocation of method arguments, but I think in practice this is always going to be true, no? A ton of logged arguments are string literals, but there are also a ton of arbitrary values, themselves sometimes interfaces.

@08d2, many common uses of LogAttrs will have no heap allocation.

rliebz commented 1 year ago

@jba is there a way for a handler to get at the log's full stack trace?

I'm hoping to be able to build a handler to generate a stack trace for the log rather than just the file/line, ideally with enough flexibility to define the string format (to match stack traces my applications are generating today for errors, which is modeled after stack traces from panics). From what I can tell, the program counter and runtime frames are never exported and only indirectly exposed through Record.SourceLine. A *runtime.Frames would be perfect in my case, but maybe that's something that is intentionally not exposed through slog's API.

08d2 commented 1 year ago

@zephyrtronium

What I mean by "storage" is the actual memory locations used to pass arguments to the methods. Because slog.Logger methods take ...any or ...Attr arguments, calling those methods through an interface forces the slices implementing the ... parameters to be heap-allocated. Each element of those slices also has to contain a heap-allocated object (because interfaces always store pointers, currently with the exception of one-byte integers). It is possible to preallocate and reuse those slices and even the interface boxes, but doing so takes quite a few lines of code.

Ah! I see what you mean. Yep: variadic parameters to methods of interfaces are heap-allocated by default. But this isn't a requirement of the language, it's a detail of the current implementation. If -- or, rather, when -- escape analysis improves, and this property is no longer true, is the API proposed here still a good one? More broadly, is it appropriate to define something in the stdlib which relies on details of the current implementation and isn't guaranteed by the spec?

ed:

I don't think loggers are at all "core and ubiquitous" in the same way as interface types. I have written very few programs and packages that don't use interfaces, and exactly one that does use anything resembling structured logging

Have you written programs that don't log at all? Structured logging is a strict superset of "unstructured" logging, not a categorically different thing. Every program that logs via log.Printf(x) or fmt.Println(x) or whatever, can express that logging equivalently via e.g. slog.Log("msg", x) with no practical downside costs.

jba commented 1 year ago

Why not LevelDebug, LevelInfo etc. rather than DebugLevel, InfoLevel etc?

@leinadao, see https://go.dev/cl/456620.

jba commented 1 year ago

Is there a way for a handler to get at the log's full stack trace?

@rliebz, we intentionally omitted it because you can get it with the usual mechanisms: runtime.Callers, runtime.Stack, etc.

OscarVanL commented 1 year ago

Is it a deliberate decision that slog is missing a Panic function?

jba commented 1 year ago

Is it a deliberate decision that slog is missing a Panic function?

Yes. We wanted to separate control flow from logging. Including control flow adds complexity, notably around flushing.

rsc commented 1 year ago

Are there any remaining concerns about accepting this proposal?

ChrisHines commented 1 year ago

@rsc How much time would I have to provide feedback. As one of the main authors of Go kit log, contributor to log15 and having worked on a couple of non-public logging packages as well I feel somewhat of an obligation to review this proposal. It is a big proposal, though, and I've been putting off engaging because of the time required to consider all of it from the point-of-view of the standard library and Go project and then provide constructive comments. I also knew a proposal like this would garner a lot of attention and comments and I just didn't have the time to keep up with all of that.

It seems that perhaps things are settling down now, though, so it might be a good time for me to take more than a cursory look at the design.

rsc commented 1 year ago

@ChrisHines Thanks for offering to review it. It's been open for almost two months, and the discussion here seems to have quieted down without obvious objections. In the absence of showstoppers, I would hope that the proposal process would wrap up some time in January. We definitely want your input and to learn from your experience. @jba may reach out to set up a higher-bandwidth conversation.

jellevandenhooff commented 1 year ago

Re-reading this issue, the logger-in-context or log-attributes-in-context conversation still does not feel satisfyingly resolved.

Several comments mention requirements for the package related to context:

A number of comments (starting at https://github.com/golang/go/issues/56345#issuecomment-1289572644, https://github.com/golang/go/issues/56345#issuecomment-1292672956, mine at https://github.com/golang/go/issues/56345#issuecomment-1322817880) suggest variations on storing attributes on the context instead of the entire logger. Some of the motivation is to aesthetically not store big structs on the context, while my motivation was to be able to combine attributes from a logger on a long-lived application struct with short-lived request-scoped attributes from a logger on the context.

I've tried to summarize the issue at https://github.com/golang/go/issues/56345#issuecomment-1336539184: since loggers are not mergeable, application code is going to have to choose at points between attributes stored on a long-lived struct logger or a context logger, and it will have to error-prone re-add the missing attributes. That seems like an unnecessary limitation of the API. Storing attributes on the context instead of the logger would fix that problem, and I believe could still address all requirements mentioned above.

It feels hard to really explore this design choice looking at smaller toy examples. I don't think I fully understand the concern regarding having to pass both loggers and context in https://github.com/golang/go/issues/56345#issuecomment-1328100306, an example there would be helpful to me.

deefdragon commented 1 year ago

I believe the dissent around inline key-value pairs is also unresolved. While I have probably been the loudest voice for their removal, I have only seen one person speak in favor of inlined key values, and many 👍 and comments discussing disapproval of inline key-value.

To put this to bed, I (officially I guess) suggest the following alterations to the API. TLDR; remove ...any and add in ...Attr from all applicable calls.

- func Debug(msg string, args ...any)
- func Error(msg string, err error, args ...any)
- func Info(msg string, args ...any)
- func Log(level Level, msg string, args ...any)
- func Warn(msg string, args ...any)

+ func Debug(msg string, args ...Attr)
+ func Error(msg string, err error, args ...Attr)
+ func Info(msg string, args ...Attr)
+ func Log(level Level, msg string, args ...Attr)
+ func Warn(msg string, args ...Attr)

type Logger
- func With(args ...any) *Logger
+ func With(args ...Attr) *Logger

- func (l *Logger) Debug(msg string, args ...any)
- func (l *Logger) Error(msg string, err error, args ...any)
- func (l *Logger) Info(msg string, args ...any)
- func (l *Logger) Log(level Level, msg string, args ...any)
- func (l *Logger) Warn(msg string, args ...any)
- func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...any)
- func (l *Logger) With(args ...any) *Logger

+ func (l *Logger) Debug(msg string, args ...Attr)
+ func (l *Logger) Error(msg string, err error, args ...Attr)
+ func (l *Logger) Info(msg string, args ...Attr)
+ func (l *Logger) Log(level Level, msg string, args ...Attr)
+ func (l *Logger) Warn(msg string, args ...Attr)
+ func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...Attr)
+ func (l *Logger) With(args ...Attr) *Logger

If there is a large enough demand, WithKV(args ...any) could be discussed as an addition later, but I did not include it here to keep the emotes and comments focused on just the switch of any and Attr.

Edit: There are many people who have shown support of sugared logging. Thus adding a way to make sugaring easy for those who wish to use it is important, so I amend this to include adding the WithKV method I originally mentioned.

+ func (l *Logger) WithKV(args ...any) *Logger
jba commented 1 year ago

@jellevandenhooff, I think the link to your summary is https://github.com/golang/go/issues/56345#issuecomment-1336539184.

hherman1 commented 1 year ago

@deefdragon I have not spoken in support of in line key value pairs because that is what was already proposed. I imagine there are others like me

beoran commented 1 year ago

@deefdragon While we are at it, I would let all methods on Logger return the logger for chaining. Or somehow pass the attributes in a chained fashion. While I admit that a Zerolog like API could cause resource leaks, there might be a way to construct a safe chained API.

jba commented 1 year ago

Storing attributes on the context instead of the logger ...

@jellevandenhooff, let me explain why we support storing the logger in the context, elaborating on https://github.com/golang/go/issues/56345#issuecomment-1328100306 where I wrote

I think you'll find that people will now have to pass both Loggers and contexts around together. From there it's hard to resist adding the Logger to the context.

To log something you need both a list of attributes and a way to format the log output—a Handler. It is useful to put both of these in the context, because they tend to follow the call chain: I may add an attribute that I want logged by every function I call, or I may want those functions to use a different Handler (to change the minimum level, for example), or both. We could store both the attributes and the Handler in the context, but that requires two expensive context lookups. How about we combine them into one value, and put that in the context? That value is a Logger.

You say there is a flaw in slog's design, because it makes it hard to merge two loggers. But we can flip that around: I would say that given the independent justification for slog's design I've provided above, there is a flaw in your server's design, because it creates the need for the merge. Either pass the logger around in the context and put attributes in your struct, or put the logger in the struct and pass only attributes in the context. You can do either with slog.

jba commented 1 year ago

I have only seen one person speak in favor of inlined key values, and many 👍 and comments discussing disapproval of inline key-value.

This concern reminds me of the discussion we had on the error values proposal about the %w format directive for fmt.Errorf. Many people felt that it was not type-safe or general enough and advocated for a function like errors.Wrap. In the end we stuck with %w over the objections, and that seems to have been the right call: it is indeed type-unsafe, but that doesn't actually seem to bother anyone in practice (and there is a vet check—sound familiar?). And it is so much easier to use than a function.

So @deefdragon, while I agree that on this issue there has been more disapproval than approval, we have to take into account the millions of Go programmers who don't follow these issues (and those who do—thanks @hherman1) who rely on Go to be lightweight and easy to read and write. Without inlined key-value pairs, the API would feel cumbersome, and many would avoid it. Structured logging is already more verbose than printf-style logging; we don't want to add to that burden. The situation is actually better than with the error proposal, because slog does provide the alternative.

We also know from the existence of other popular structured logging packages that use key-value pairs—go-kit/log, logr, hclog and Zap's SugaredLogger—that people are generally fine with them.

jellevandenhooff commented 1 year ago

Re. storing attributes on the context instead of the logger

To log something you need both a list of attributes and a way to format the log output—a Handler. It is useful to put both of these in the context, because they tend to follow the call chain: I may add an attribute that I want logged by every function I call, or I may want those functions to use a different Handler (to change the minimum level, for example), or both. We could store both the attributes and the Handler in the context, but that requires two expensive context lookups. How about we combine them into one value, and put that in the context? That value is a Logger.

Thanks @jba, this performance argument was helpful to me. The optimization of pre-formatting into JSON or text that the current Handler.With calls performs also seems hard to recreate with attributes stored in the context. The ability to set a DEBUG-level for an entire request is nice as well.

I can think of some performance optimizations to try and get the best of all worlds, though they sure become clunky: a Logger-like value could combine the attributes and the handler but still be updatable with individual API calls; the pre-formatting could happen outside of the handler in a combinable buffer; the context could include an override log-level. At that point you might as well store the logger in the context and write code to merge two loggers :)

You say there is a flaw in slog's design, because it makes it hard to merge two loggers. But we can flip that around: I would say that given the independent justification for slog's design I've provided above, there is a flaw in your server's design, because it creates the need for the merge. Either pass the logger around in the context and put attributes in your struct, or put the logger in the struct and pass only attributes in the context. You can do either with slog.

I described the inability to merge two loggers and the logger-in-context API as an "unnecessary limitation." That was not helpful and I apologize.

I understand that the API provides two different approaches, and applications can choose the approach that fits them best. I wish the choice wasn't necessary, as it's another design choice to make, and different dependencies might force incompatible decisions on the application developer. I was hoping the two approaches could be compatible and the choice unnecessary, but it seems not.

Thanks again for the work on the design and your thoughts here.

(Your mention of the level adjusting does remind me that it might be helpful to set a DEBUG-level log for a specific package, which I think is more easily doable with the logger-in-struct approach using a LevelVar per package. If the API somehow let you make toggle this at the package and request level at the same time, that would be of course amazing.)

szmcdull commented 1 year ago

How to change the level of the defaultLogger

szmcdull commented 1 year ago

I'm writing a log router. I found no way to modify Record. I need to store groups info into Record and pass it down to backend Handler.

routedLogger.With(`id`, 345).WithGroup(`Group C`).Debug(`with group msg`, `key3`, `val3`)
szmcdull commented 1 year ago

Please make more methods and structs public. It is not so easy to reuse existing slog feature, and customize and extend it right now.

For example, newDefaultHandler (to keep default format but change output), Logger.logDepthErr (to log err with other level), commonHandler (to reuse group/attrs handling) ...

wizhi commented 1 year ago

How to change the level of the defaultLogger

@szmcdull You construct your own *slog.Logger with a slog.Handler that has your level enabled, and then use the slog.SetDefault(*slog.Logger) function.

h := slog.HandlerOptions{Level: slog.LevelDebug}.NewTextHandler(os.Stderr)
l := slog.New(h)

slog.SetDefault(l)

slog.Debug("development mode")

It is not so easy to reuse existing slog feature, and customize and extend it right now.

You can see some examples of doing this on the wiki.

ainar-g commented 1 year ago

@jba, pardon for the long time it took me to respond to your comment. I can see that since then a few issues that I'd brought up have already been resolved, which is great. With that said, I still think that the default handler should be accessible in some way.

The default handler is there just so that the slog and log packages can work together. Programs that use log can do so with the assurance that packages using the top-level slog functions like slog.Info and so on will produce output that looks like log output and goes to the same place (because it actually goes through the log package). We expect that programs that use slog will set their own handler early, so no output will go to the default handler.

I understand this reasoning, but it doesn't actually address my main issue. In my experience, the people who are the most likely to look at debug output are developers, and so the output should lean towards being human-readable. The output of TextHandler, on the other hand, is more machine-readable but rather noisy for the human eye.

If there is absolutely no desire to export defaultHandler then I would like the team to at least consider adding some way to lower its level to LevelDebug. Something like slog.DefaultToDebug(true). That would solve the human-readable debug output issue without the need to commit to defaultHandler's API and output format stability.

With that said, I have no further comments or API proposals.

jba commented 1 year ago

I found no way to modify Record.

@szmcdull, You can set the public fields of Record and use Record.AddAttrs to add new attributes. [Edit]: If you need to use both the original and modified Records, call Record.Clone before modifying.

To modify existing attributes, use NewRecord to build up a new record with your changes.

jba commented 1 year ago

Please make more methods and structs public. It is not so easy to reuse existing slog feature, and customize and extend it right now.

@szmcdull, the problem is that once something is public, it is public forever. But a private symbol can be made public any time.

For example, newDefaultHandler (to keep default format but change output)

log.SetOutput should do that.

Logger.logDepthErr (to log err with other level)

You can write slog.Warn(message, slog.ErrorKey, err, ...) or slog.LogAttrs(slog.LevelWarn, message, slog.Any(slog.ErrorKey, err).

commonHandler (to reuse group/attrs handling) ...

commonHandler is there because the two built-in handlers have a lot in common, but we don't know yet if many handlers will share that commonality. It's better to wait and see if there are useful bits that can be factored out. Meanwhile, I recommend you copy what's useful to you. Perhaps you will find improvements that fit your use case better.

jba commented 1 year ago

consider adding some way to lower [the default Handler's] level to LevelDebug

@ainar-g, You can use the example LevelHandler for that. (If you do, there is a known bug with source line information that we can resolve once in the standard library.)

ainar-g commented 1 year ago

@jba, thanks for mentioning it again. I've seen that example, but couldn't quite figure out how to use it until now. There is an issue though. What I currently do is:

h := slog.Default().Handler()
l := slog.New(NewLevelHandler(slog.LevelDebug, h))
slog.SetDefault(l)
slog.Debug("development mode")

But that immediately results in a deadlock. Inspecting slog.SetDefault, I can see that there is a check for *defaultHandler that is supposed to prevent that, but if wrappers that can wrap the default handler are allowed then this check is not enough.

One way to solve this would be to assert behavior as opposed to identity. That is, something along the lines of:

func SetDefault(l *Logger) {
        // …
        if w, ok := l.Handler().(interface {
                WrapsDefaultHandler() bool
        }); ok && w.WrapsDefaultHandler() {
                return
        }

        log.SetOutput(&handlerWriter{l.Handler(), log.Flags()})
        log.SetFlags(0)
}

(handlerWriter might also require change, since it uses LevelInfo directly.)

At that point, that might start to feel like way too much boilerplate just to get good, log-compatible plain-text debug logging.