-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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/alt_experimental_level: concept demonstration #444
Conversation
Demonstrate an alternate approach to the "level" package offered via the "github.com/go-kit/kit/log/experimental_level" import path, allowing log event level filtering via instances of the level.leveler interface, created by functions that specify the filtering threshold. Features not accommodated here for now: - Returning an error from log.Log when squelching an event - Returning an error from log.Log for events that lack a level - Documentation Benchmarks demonstrate both run time and allocation reduction for the disallowed/squelching cases compared to the "experimental_level" alternate: name old time/op new time/op delta NopBaseline-8 459ns ± 0% 130ns ± 0% -71.68% NopDisallowedLevel-8 509ns ± 0% 136ns ± 0% -73.28% NopAllowedLevel-8 515ns ± 0% 494ns ± 0% -4.08% JSONBaseline-8 2.41µs ± 0% 0.13µs ± 0% -94.45% JSONDisallowedLevel-8 506ns ± 0% 136ns ± 0% -73.12% JSONAllowedLevel-8 2.49µs ± 0% 2.45µs ± 0% -1.48% LogfmtBaseline-8 1.08µs ± 0% 0.13µs ± 0% -87.92% LogfmtDisallowedLevel-8 507ns ± 0% 136ns ± 0% -73.18% LogfmtAllowedLevel-8 1.13µs ± 0% 1.10µs ± 0% -3.26% name old alloc/op new alloc/op delta NopBaseline-8 288B ± 0% 64B ± 0% -77.78% NopDisallowedLevel-8 288B ± 0% 64B ± 0% -77.78% NopAllowedLevel-8 288B ± 0% 288B ± 0% +0.00% JSONBaseline-8 968B ± 0% 64B ± 0% -93.39% JSONDisallowedLevel-8 288B ± 0% 64B ± 0% -77.78% JSONAllowedLevel-8 968B ± 0% 968B ± 0% +0.00% LogfmtBaseline-8 288B ± 0% 64B ± 0% -77.78% LogfmtDisallowedLevel-8 288B ± 0% 64B ± 0% -77.78% LogfmtAllowedLevel-8 288B ± 0% 288B ± 0% +0.00% name old allocs/op new allocs/op delta NopBaseline-8 9.00 ± 0% 3.00 ± 0% -66.67% NopDisallowedLevel-8 9.00 ± 0% 3.00 ± 0% -66.67% NopAllowedLevel-8 9.00 ± 0% 9.00 ± 0% +0.00% JSONBaseline-8 22.0 ± 0% 3.0 ± 0% -86.36% JSONDisallowedLevel-8 9.00 ± 0% 3.00 ± 0% -66.67% JSONAllowedLevel-8 22.0 ± 0% 22.0 ± 0% +0.00% LogfmtBaseline-8 9.00 ± 0% 3.00 ± 0% -66.67% LogfmtDisallowedLevel-8 9.00 ± 0% 3.00 ± 0% -66.67% LogfmtAllowedLevel-8 9.00 ± 0% 9.00 ± 0% +0.00%
Taking a walk immediately after posting the first commit made me realize that this probably doesn't play well with I'll have to do more experimentation over the weekend to see whether that's a fatal flaw, or one that's surmountable. |
What I can't see how to surmount with this design is the opaque relationship between a logger := level.AllowingAll(logger)
// ...
level.Debug(logger).Log("msg", "You will see this.")
level.Debug(log.NewContext(logger)).Log("msg", "You'll never see this.") For my proposed The |
Thanks for this. If it were just performance improvements I'd say we should merge the existing experimental_level, but keep this one open and keep hacking on it to see if we can arrive at a solution. But it's also an API change, and in a direction that I think I prefer. This complicates things. Given I want to get something merged for 1.0.0 in O(days), and I'd like to avoid future breaking changes if possible, what do you think the best choice is here? If you think a day or two of applied hacking will make this work, I'm happy to do a spike. If you think it requires more thought, I'm also happy to merge the existing experimental_level and sort of resign myself to a potential breaking change (and version bump) in a month or two. Leaning toward option 2 but eager for your feedback. |
@seh Any comment on the above? |
I am excited to hear that you find my proposal intriguing enough to warrant refining it. I am not sure yet that the idea will bear fruit, but there's one more interface that I'd like to work on adding tomorrow (more easily attempted at the office) that's analogous to the "pkg/errors" package's
Can you tell what you see here that you prefer? I'm not looking for flattery; I just want to know which parts to preserve and emphasize and which parts to eliminate (such as the global state you and @ChrisHines detested from the earlier #368). Please allow me another day to either show you a more complete solution or declare that the idea isn't worth pursuing further. |
I like that you've implemented the level-setting behavior using something like a decorator. The UX just feels a lot more natural than the existing "invoke a function to get an opaque slice of strings" method. That said, my method allows user-specified levels with a lot less fuss, which I like; though it's not clear to me if anyone would ever bother with it :) One thing I'd still like to see—and which the existing impl also doesn't have—is a nice way to bind typical commandline flags to levels. In practice I think this would boil down to a helper function that converts a user-given string like logLevel = flag.String("log-level", "warn", "debug, info, warn, error, or none")
// ...
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = level.AllowingLevel(*logLevel)(logger) // or...
logger = level.AllowingLevelDefault(*logLevel, AllowingErrorOnly)(logger) Something like this. |
I've made changes to the primary code that allow it to accommodate wrapped I'd like to write a few more unit tests before pushing a patch. That will likely take me through early tomorrow morning. |
Introduce the "log.Delegate" function to extract a wrapped delegate Logger instance from a decorating instance, allowing the level-related functions to find the configuration they need within the outermost Logger created by one of the leveled factory functions. Benchmarks demonstrate both run time and allocation reduction for the disallowed/squelching cases compared to the "experimental_level" alternate: name old time/op new time/op delta NopBaseline-8 470ns ± 0% 494ns ± 0% +5.11% NopDisallowedLevel-8 513ns ± 0% 123ns ± 0% -76.02% NopAllowedLevel-8 516ns ± 0% 474ns ± 0% -8.14% JSONBaseline-8 2.44µs ± 0% 2.39µs ± 0% -2.45% JSONDisallowedLevel-8 515ns ± 0% 123ns ± 0% -76.12% JSONAllowedLevel-8 2.48µs ± 0% 2.33µs ± 0% -5.97% LogfmtBaseline-8 1.05µs ± 0% 1.03µs ± 0% -2.19% LogfmtDisallowedLevel-8 514ns ± 0% 121ns ± 0% -76.46% LogfmtAllowedLevel-8 1.13µs ± 0% 1.02µs ± 0% -9.39% name old alloc/op new alloc/op delta NopBaseline-8 288B ± 0% 288B ± 0% +0.00% NopDisallowedLevel-8 288B ± 0% 64B ± 0% -77.78% NopAllowedLevel-8 288B ± 0% 288B ± 0% +0.00% JSONBaseline-8 968B ± 0% 968B ± 0% +0.00% JSONDisallowedLevel-8 288B ± 0% 64B ± 0% -77.78% JSONAllowedLevel-8 968B ± 0% 968B ± 0% +0.00% LogfmtBaseline-8 288B ± 0% 288B ± 0% +0.00% LogfmtDisallowedLevel-8 288B ± 0% 64B ± 0% -77.78% LogfmtAllowedLevel-8 288B ± 0% 288B ± 0% +0.00% name old allocs/op new allocs/op delta NopBaseline-8 9.00 ± 0% 9.00 ± 0% +0.00% NopDisallowedLevel-8 9.00 ± 0% 3.00 ± 0% -66.67% NopAllowedLevel-8 9.00 ± 0% 9.00 ± 0% +0.00% JSONBaseline-8 22.0 ± 0% 22.0 ± 0% +0.00% JSONDisallowedLevel-8 9.00 ± 0% 3.00 ± 0% -66.67% JSONAllowedLevel-8 22.0 ± 0% 22.0 ± 0% +0.00% LogfmtBaseline-8 9.00 ± 0% 9.00 ± 0% +0.00% LogfmtDisallowedLevel-8 9.00 ± 0% 3.00 ± 0% -66.67% LogfmtAllowedLevel-8 9.00 ± 0% 9.00 ± 0% +0.00% The running time increase in the "NopBaseline-8" benchmare is due to the level.Debug function doing more work, needing to inspect the supplied log.Logger to see whether it wraps a level restriction that would preclude preceding with creating a log.Context to record the level.
Commit c6bd2a9 introduces a potentially controversial pair of exported functions:
With these functions in place, it's possible to "peel a @peterbourgon and @ChrisHines, please let me know if you find this proposal an insulting affront to the "log" package's otherwise clean surface, or whether it's an acceptable trade of abstraction for capability. |
Convince me that inspecting keyvals is a problem. |
Well, I can cite two deficiencies that I dislike, but whether you'll consider them sufficient evidence is predicated on how time- or allocation-sensitive your application is. We've touched on this before. Restating my complaints:
The benchmarks show a reduction in CPU time and allocation. However, maybe it's not enough of a reduction to convince you that's worth it. Now, maybe you're suggesting that I could instead tuck the state I need into a key/value pair in a |
@seh: First, let me be unequivocal in my thanks to you for working on this issue. I am delighted to have others trying to improve the library. :) Your benchmark improvements are attractive. The API changes to package I am reminded of a Slack conversation we had last August in which I proposed yet another alternative that we haven't tried yet. In that conversation I mused:
I wonder if that could be made to work, what its performance would be, and if it would be less intrusive and also more general? Perhaps a way forward here (if possible) is to implement something that is functionally correct and has the sleeker |
Would something along these lines be considered as an alternative: https://gist.github.com/freeformz/3334b5a88602c08927b420de665224dd |
It does, though it does not force others to do so. However, if they don't implement type myLogger struct {
log.Logger
}
func sneakAroundRestriction(logger log.Logger) {
logger = level.AllowingErrorOnly(logger)
logger = myLogger{ logger }
level.Debug(logger).Log("msg", "Surprise!")
}
I haven't look at |
@freeformz I don't completely understand what you are proposing. Could you provide more detail in a separate issue? @seh Yes, I was asking if it was possible to implement the behaviors and API of this PR without changing package |
It probably is possible to do the filtering, given what's already present over in the experimental_level implementation, but without the same short-circuiting behavior. Callers that neither use custom I'll need a few more hours to think about it and try it out. I'll report back here with what I learn. |
@ChrisHines I am effectively proposing an alternative to this proposal that doesn't expand the core API of log.Logger. To be fair though I only found this issue not long before posting the gist, which is purely a design foil, and not an implementation. So feel free to ignore. I am willing to implement though as a PR if there is interest. |
I'm confused by that statement; your proposal appears to introduce a very important new function in the func Log(level Level, keyvals ...interface{}) error |
@seh, no I am not. it's just another part of |
@seh I'm spending some time playing with the code in this PR. Here is a test that fails. func TestMultiLoggerDifferentLevels(t *testing.T) {
var out1, out2 [][]interface{}
logger1 := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error {
out1 = append(out1, keyvals)
return nil
}))
logger2 := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error {
out2 = append(out2, keyvals)
return nil
}))
logger1 = level.AllowingErrorOnly(logger1)
logger2 = level.AllowingAll(logger2)
multi := NewMultiLogger(logger1, logger2)
level.Error(multi).Log("foo", "bar")
level.Info(multi).Log("foo", "bar")
if got, want := len(out1), 1; got != want {
t.Errorf("out1 len: got %v, want %v, data %v", got, want, out1)
}
if got, want := len(out2), 2; got != want {
t.Errorf("out2 len: got %v, want %v, data %v", got, want, out2)
}
}
type multiLogger []log.Logger
func (l multiLogger) Log(keyvals ...interface{}) error {
for _, logger := range l {
logger.Log(keyvals...)
}
return nil
}
func NewMultiLogger(loggers ...log.Logger) log.Logger {
return multiLogger(loggers)
} It would be nice to support this use case. It is easily accomplished with log15 and is also similar to the default behavior of glog. Thoughts? |
I am not ignoring you deliberately, @ChrisHines; I've read the test case (nicely written, by the way), but haven't had enough spare time today to concentrate on it. Perhaps tomorrow morning will afford me a better chance to look into it with the attention it deserves. In private, I'd be happy to complain about what's demanding my attention by day. |
@seh No worries. We're all busy. |
I finally have some time to write down what I wanted to say shortly after you posted your multi-logger example. As you asked and I responded here, in this case it matters gravely whether type It's not enough to implement a func Delegate() []log.Logger If Go were a different language with a generic programming facility at this point, I might suggest instead something more like this: func ReduceDelegates(f func(T result, logger log.Logger) T, T initial) T Rather than I started thinking through how to write that maximizing graph traversal. It would benefit from memoization to preclude inspecting the structure again and again. That explains why your example doesn't work correctly. The next question is whether you think it's worth it to try to make it work, or whether it's sufficiently damning evidence against the |
My feeling at this point is that the complexity costs outweigh the gains for this sort of optimization. I suspect there are more configurations that pose additional challenges and require even more complexity to work around. The acknowledged problem with having to adjust log.Caller is another ding against this PR. I don't want our I do appreciate the work you have done to explore this idea. You've done a fantastic job with the code, benchmarking and communicating your idea. Thanks! |
Thank you. On the subject of |
At one point we exported a |
@seh said:
Note that it only needs that when the |
I spent a while yesterday experimenting with the idea of teaching While that log record dropping is an appealing facility to generalize like this, it still falls prey to the same problem I ran into in my earlier attempts at solving this level filtering problem: a free function like My goal has been to find a way to short-circuit the building up of key/value pairs in Introducing a way for a
Note second part: "if their level is filtered." How does a The I haven't given up yet, but I am much less certain that there's a big optimization here to be captured. I'll work on it more today to see whether any of my other ideas bear fruit. |
@seh Nice summary of the problem. I too have played with the sentinel value idea and have come to the same conclusions as you. It won't work without global state or a stateful layer above the As you point out, trying to optimize away the construction of a keyvals slice cannot be done in general because the next |
I did come up with yet another approach. As of the end of the day when I have to stop, the benchmarks show some improvements compared to the experimental_level implementation, but in some cases this one is worse. I'll look over it tomorrow to see if I can figure out how to fix those cases where we do worse here. It takes advantage of installing an optional projection function into More on this tomorrow. |
Please see #453. |
As a foil to #357 and following up on #368, demonstrate an alternate approach to the "level" package offered via the "github.com/go-kit/kit/log/experimental_package" import path, allowing log event level filtering via instances of the (unexported)
level.leveler
interface, created by functions that specify the filtering threshold.Features not accommodated here for now:
(This feature doesn't make much sense with this approach.)
(This feature doesn't make much sense with this approach.)
(We could adapt much of this from the sibling package.)
See the commit message for then benchstat comparison.