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/experimental_level #357

Merged
merged 7 commits into from
Sep 7, 2016
Merged

log/experimental_level #357

merged 7 commits into from
Sep 7, 2016

Conversation

peterbourgon
Copy link
Member

@peterbourgon peterbourgon commented Sep 6, 2016

Still need to get some more tests and benchmarks in, but this API feels usable.

At the callsite, things work like this.

import (
    "github.com/go-kit/kit/log"
    "github.com/go-kit/kit/log/experimental_level"
)

func foo(logger log.Logger) {
    level.Debug(logger).Log("foo", "bar")  // level=debug foo=bar
    level.Error(logger).Log("baz", "quux") // level=error baz=quux
}

The filtering comes in at construction time.

var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = level.New(logger, level.Config{Allowed: level.AllowWarnAndAbove})
logger = log.NewContext(logger).With("ts", log.DefaultTimestampUTC)

The Config object allows precise control over the behavior when callers log without a level, or log with a disallowed level; see that struct for details. And while the level package includes default Debug, Info, Warn, and Error levels, the user is free to define their own level values, provide whatever wrappers they like to the constructors, and define their own helper functions. It's all pretty flat.

type Config struct {
// Allowed enumerates the accepted log levels. If a log event is encountered
// with a LevelKey set to a value that isn't explicitly allowed, the event
// will be equelched, and ErrSquelched returned.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/equelched/squelched/ ?

@peterbourgon
Copy link
Member Author

peterbourgon commented Sep 6, 2016

$ go test -bench=. -benchmem
BenchmarkNopBaseline-4             2000000    621 ns/op   288 B/op    9 allocs/op
BenchmarkNopDisallowedLevel-4      2000000    674 ns/op   288 B/op    9 allocs/op
BenchmarkNopAllowedLevel-4         2000000    678 ns/op   288 B/op    9 allocs/op
BenchmarkJSONBaseline-4             500000   3186 ns/op   968 B/op   22 allocs/op
BenchmarkJSONDisallowedLevel-4     2000000    725 ns/op   288 B/op    9 allocs/op
BenchmarkJSONAllowedLevel-4         500000   3230 ns/op   968 B/op   22 allocs/op
BenchmarkLogfmtBaseline-4          1000000   1326 ns/op   288 B/op    9 allocs/op
BenchmarkLogfmtDisallowedLevel-4   2000000    671 ns/op   288 B/op    9 allocs/op
BenchmarkLogfmtAllowedLevel-4      1000000   1400 ns/op   288 B/op    9 allocs/op

A good start; could be better.

@peterbourgon
Copy link
Member Author

Ahoy, @ChrisHines @jstordeur @nelz9999 — this PR represents my vision for leveled logging. To the best of my ability it abides the guidelines set forth in the package log README section about enhancements, though it's certainly possible that I've overlooked something. Thus I'd love some early feedback, in particular things you want to do that seem difficult or impossible with this API, and/or any concerns you have about allocations.

@peterbourgon
Copy link
Member Author

peterbourgon commented Sep 6, 2016

Also cc: @AlmogBaku for your feedback, if you care to provide it :)

@peterbourgon
Copy link
Member Author

peterbourgon commented Sep 6, 2016

Also cc: @seh for your feedback, if you care to provide it :)

@ChrisHines
Copy link
Member

Comments from my first reading:

  • The default choice to squelch, without error, log events without a level key seems dangerous. I can imagine many frustrations when retrofitting an existing project with levels. Could you explain the rationale behind this choice a bit?
  • I am a little concerned about using global variables for LevelKey and *LevelValue. I realize that it enables the nice level.Debug(logger).Log() usage style, but I wonder if it will stand the test of time.
  • AllowAll, AllowDebugAndAbove, etc. are fragile. They will quietly break if not updated when one of ErrorLevelValue, WarnLevelValue, etc. are given new values.

logger = log.NewContext(logger).With("caller", log.DefaultCaller)

level.Info(logger).Log("foo", "bar")
if want, have := `caller=level_test.go:134 level=info foo=bar`, strings.TrimSpace(buf.String()); want != have {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, this looks brittle... If anyone adds/removes non-zero number of lines above this, the line number will get out of sync.

@nelz9999
Copy link
Contributor

nelz9999 commented Sep 7, 2016

In general, I really like this as an elegant way of eliding all the different concerns. Thank you!

I agree with @ChrisHines that defaulting to squelch non-"level" logs quietly is challenging. Do you think there would be negative impact on inverting AllowNoLevel so that it defaults to not squelching? (I.e. DisallowNoLevel...)

I think it would be possible to have levelKey be configurable by making a member var on the logger struct, initialized to the default in the New(...) function if the config doesn't have an override for "level".

@peterbourgon
Copy link
Member Author

Thanks for the feedback! Exactly what I needed, thanks.

@ChrisHines

The default choice to squelch, without error, log events without a level key seems dangerous . . .

Yes—and yes to similar feedback from @nelz9999. Upon reflection, I agree. Updated.

I am a little concerned about using global variables for LevelKey and *LevelValue. I realize that it enables the nice level.Debug(logger).Log() usage style, but I wonder if it will stand the test of time.

It's true. I'd love to be able to do as @nelz9999 suggests and have the level key be part of the config struct. But, the UX of level.Debug(logger).Log is absolutely foundational to this proposal, and I can't think of a way to parameterize the level key in a way that preserves it. I'll keep thinking. For now I've made levelKey an unexported const, as in practice I can't think of many who would want to change it.

AllowAll, AllowDebugAndAbove, etc. are fragile. They will quietly break if not updated when one of ErrorLevelValue, WarnLevelValue, etc. are given new values.

Great point. I've also unexported and made const the various fooLevelValues, and changed the AllowFoo vars to functions. This makes the level value strings a bit more difficult to change—users will have to replace the whole value chain, so to speak—but in practice I think no one would actually do that, and it makes things safer, as you note.


@nelz9999

Oof, this looks brittle...

Yes! It is. But we've done similar things elsewhere in the project, and the nice thing is when it breaks you know exactly why :)


I'm considering if it might be better to have a default-allow, rather than default-deny policy. That would lead to enumerating a Disallowed (c.f. Allowed) set of levels. But I need to think on that a bit more.

Also considering if it might make sense to have some way to declare that all un-leveled log events should get a specified level. But that may have too-complex interactions with other decisions that are important for UX. Need to think there, too.

- Unexport level key and default level values
- Migrate Allow vars to functions
- By default, un-leveled log events are allowed
@nelz9999
Copy link
Contributor

nelz9999 commented Sep 7, 2016

So, one of the things I think I'm seeing in both the original levels package, and in this one, is that these "extensions" are all having trouble because they are trying to wrap their own configuration around a log.Context that they then have to delegate to in order to do it's actual logging.

But what if the log.Context had a way to store arbitrary values on it, kind of like the stdlib context.WithValue(...)? You could still use a package level function at construction time, then the logger that is handed around would remain the vehicle for all state, and then do the same level.Debug(logger).Log at the callsite as you have here.

I know this impacts a higher package, but it seems like a very flexible way of allowing arbitrary extensions built on top of the log package.

@peterbourgon peterbourgon changed the title [WIP] log/experimental_level log/experimental_level Sep 7, 2016
@AlmogBaku
Copy link

AlmogBaku commented Sep 7, 2016

Hi,
TBH I have a little hesitations about the API, which doesn't looks so standard:

level.Debug(logger).Log("foo", "bar")
//Instead of standard API like
logger.Debug("foo", "bar")

logger = level.New(logger, level.Config{Allowed: level.AllowWarnAndAbove})
//Instead of
logger = level.New(logger, level.Config{Level: level.DEBUG})

I find it a little bit confusing from DX, since the majority of the loggers from any language I know looks pretty similar..

Other Notes

  1. I liked the idea that you can specify an array of allowed verbosity levels, however it might be redundant in case you want to use "hierarchy of logs", also it introduced a complexer decision making based on iterating the log level String over the "allowed logs"
  2. I'm not sure why when log is not allowed it returns an error.. it's just should be ignored. That's may confuse me as an user
  3. I believe log-level should be int value and then to be translated into a string. This way you can do mathematical equation which is quite effective

Bottom line

Although we have a problem with implementing the log-levels as an implementation of Logger(as specified in #269), I believe we should try to create an api which is similar to what the standards in the industry, and not reinvent the wheel, but to find other workaround to the issues raised. I think DX is extremely important on this case.

@peterbourgon
Copy link
Member Author

@AlmogBaku logger.Debug implies we'd be changing the Logger interface, which is something I think we all want to avoid. So there will be some contortion involved—but I hear your feedback that this may be a bit too awkward. Thanks for that and I'll dwell on it.

@peterbourgon
Copy link
Member Author

I'm going to merge this as-is so a user can get sort of alpha-style access to it, and I'll start another issue to track iterations, enhancements, or potentially deleting it. Thanks again to everyone!

@peterbourgon peterbourgon merged commit d3b614b into master Sep 7, 2016
@AlmogBaku
Copy link

Sorry, for that 😞
But thanks for the PR and keep the awesome work dude!

On Wednesday, 7 September 2016, Peter Bourgon [email protected]
wrote:

I'm going to merge this as-is so a user can get sort of alpha-style access
to it, and I'll start another issue to track iterations, enhancements, or
potentially deleting it. Thanks again to everyone!


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#357 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAGCphwdgm4Dvq_atm_FbsNTq80f1fq8ks5qnq6tgaJpZM4J2Bs-
.

http://www.rimoto.net/

www.rimoto.com http://www.rimoto.net/

Almog Baku

CTO & Cofounder *
Mobile: +972.50.2288.744
Social: * http://www.facebook.com/AlmogBaku
http://www.linkedin.com/in/almogbaku

@peterbourgon peterbourgon deleted the experimental-levels branch September 7, 2016 12:40
continue
}
hasLevel = true
if i >= len(keyvals) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this check for? Unless I'm missing something I believe this is always false.

@seh
Copy link
Contributor

seh commented Oct 2, 2016

Please see #368 for an alternate approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants