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

New API for package log (post-GopherCon) #76

Merged
merged 11 commits into from Jul 15, 2015
Merged

New API for package log (post-GopherCon) #76

merged 11 commits into from Jul 15, 2015

Conversation

@peterbourgon
Copy link
Member

@peterbourgon peterbourgon commented Jul 11, 2015

// use by multiple goroutines. For this implementation that means making
// only one call to l.w.Write() for each call to Log. We first collect all
// of the bytes into b, and then call l.w.Write(b).
for i := 1; i < len(keyvals); i += 2 {

This comment has been minimized.

@Mistobaan

Mistobaan Jul 11, 2015

why is 1 here ?

This comment has been minimized.

@ChrisHines

ChrisHines Jul 11, 2015
Member

Because keyvals contains alternating keys (in even indices) and values (in odd indices). This loop only needs to operate on the values, so it can start at index 1.

@ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Jul 11, 2015

@peterbourgon I plan to work on integrating this code into the real log package. I wanted to let you know to avoid duplicated effort.

@peterbourgon
Copy link
Member Author

@peterbourgon peterbourgon commented Jul 11, 2015

@ChrisHines Sure! I'll leave it alone for the weekend.

@ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Jul 13, 2015

Still a WIP. I kept log.With instead of naming it log.NewContext. Think about which you like better.

The above commit allows

lc := log.With(logger, "k1", "v2")
lc = lc.With("k2", "v2")
lc.Log("k3", "v3")

Curiously, the following is equivalent to the above:

lc := log.With(logger, "k1", "v2")
lc = log.With(lc, "k2", "v2")   // use the package func instead of method on Context.
lc.Log("k3", "v3")

Is the convenience worth the weight of having two ways to do the same thing?

Also, should we add the following interface and define log.With to check for and return a WithLogger instead of a *Context?

type WithLogger interface {
    Logger
    With(keyvals ...interface{}) WithLogger
}
@ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Jul 13, 2015

Responding to my above comment, maybe the Context constructor should look like:

func NewContext(logger Logger) *Context

Usage would then look like below, and it would avoid the overlap between the constructor and With method without increasing verbosity much.

lc := log.NewContext(logger).With("k1", "v1")
lc = lc.With("k2", "v2")
lc.Log("k3", "v3")

I've talked myself into this change already. Commit coming soon.

@peterbourgon
Copy link
Member Author

@peterbourgon peterbourgon commented Jul 13, 2015

Feedback:

  • I vaguely like NewContext better than With, as it's more explicit about the fact that it allocates a new object.
  • I vaguely dislike package funcs that take objects as arguments because it seems to invert the natural relationship between data and behavior. That is, I expect the package to hold types and constructors, and behavior to be implemented on the types. So if you see value in log.With I guess that's fine as long as we also keep Context.With and Logger.With.
  • I don't see any reason to elide the keyval args from the constructor, as they're optional.
@ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Jul 13, 2015

@peterbourgon Thanks for the feedback. I think we already talked about your first two points at GopherCon and I'm happy with them.

As for your last point, I might have agreed until I started writing the documentation. Eliding the keyvals from the constructor simplified the docs quite a bit. In general maintaining the separation of concerns feels to me like less is more in this case. It also means that you only have to look for one identifier when searching for places where context is added.

I have a new issue to raise. I think we agreed last week that leveled logging should be split into its own package. I have started working on that change and have run into a problem. The design for leveled logging we worked on at the conference requires access to the unexported fields of log.Context to maintain the desired placement of the level key/value pair in the overall sequence. The best solution to this problem I have come up with so far is to add a Context.WithPrefix method that allows adding keyvals to the beginning of a context. Thoughts?

@ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Jul 14, 2015

The new API ideas are fully integrated and ready for review. Please take a look.

warnValue: "warn",
errorValue: "error",
critValue: "crit",
}

This comment has been minimized.

@peterbourgon

peterbourgon Jul 14, 2015
Author Member

Since we already have a Levels struct, can we put these config params directly in it, and have the Option type take a Levels struct (pointer)? One less type to worry about.

This comment has been minimized.

@ChrisHines

ChrisHines Jul 14, 2015
Member

Yes, I have combined them.

@peterbourgon
Copy link
Member Author

@peterbourgon peterbourgon commented Jul 14, 2015

I really like the feel of this. It looks like you've ported everything to package log, so I've deleted package log2. Minus that one comment about the Levels option struct, I'm good to go with this!

// level. By default, the value is "crit".
func CritValue(value string) Option {
return func(c *config) { c.critValue = value }
}

This comment has been minimized.

@tsenart

tsenart Jul 14, 2015
Contributor

My DRY spidey sense is triggering here. I appreciate the performance implications of the following suggestion, and I'd love to see a benchmark to see if we could take the hit.

type config struct {
    key string
    values map[string]string
}

var (
    DebugValue = Level("debug")
    InfoValue  = Level("info")
    WarnValue  = Level("warn")
    ErrorValue = Level("error")
    CritValue  = Level("crit")
)

func Level(level string) func(string) Option {
    return func(value string) Option {
        return func(c *config) { c.values[level] = value }
    }
}

This comment has been minimized.

@ChrisHines

ChrisHines Jul 14, 2015
Member

Clever, but I feel too clever. For better or worse, godoc currently displays functions in the package index but not variables of function type. So the function version makes the available options much easier to discover for the casual user.

// Crit logs a crit event along with keyvals.
func (l Levels) Crit(keyvals ...interface{}) error {
return l.ctx.WithPrefix(l.cfg.levelKey, l.cfg.critValue).Log(keyvals...)
}

This comment has been minimized.

@tsenart

tsenart Jul 14, 2015
Contributor

Perhaps, DRYer?

type Levels struct {
    Debug, Info, Warn, Error, Crit Logger
    ctx log.Context
    cfg *config
}

func New(logger log.Logger, options ...Option) Levels {
    // ...
    ctx := log.NewContext(logger)
    return Levels{
        Debug: ctx.WithPrefix(cfg.key, cfg.values["debug"]),
        Info:  ctx.WithPrefix(cfg.key, cfg.values["info"]),
        Warn:  ctx.WithPrefix(cfg.key, cfg.values["warn"]),
        Error: ctx.WithPrefix(cfg.key, cfg.values["error"]),
        Crit:  ctx.WithPrefix(cfg.key, cfg.values["crit"]),
        ctx:   ctx,
        cfg:   cfg,   
    }
}

This comment has been minimized.

@ChrisHines

ChrisHines Jul 14, 2015
Member

This seems to be heading back toward what we have in the master branch with a separate logger/context for each level. The downside to this approach is that each call to Levels.With must update all the leveled contexts. It seems difficult to justify the overhead of calling With on this many contexts because to break even the application must create more than five subsequent log events in the scope of the context.

@ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Jul 14, 2015

One final review and then I think we can merge this.

@peterbourgon
Copy link
Member Author

@peterbourgon peterbourgon commented Jul 15, 2015

This seems to be heading back toward what we have in the master branch with a separate logger/context for each level. The downside to this approach is that each call to Levels.With must update all the leveled contexts. It seems difficult to justify the overhead of calling With on this many contexts because to break even the application must create more than five subsequent log events in the scope of the context.

I've repeated this reasoning a couple of times in chat, now. To pre-empt further questions, can we put this explanation as a doc comment, or comment on the appropriate method/type?

@peterbourgon
Copy link
Member Author

@peterbourgon peterbourgon commented Jul 15, 2015

Otherwise :shipit:!

…heap WIth method at the expense of the Log method.
@ChrisHines ChrisHines changed the title [WIP] New API for package log (post-GopherCon) New API for package log (post-GopherCon) Jul 15, 2015
ChrisHines added a commit that referenced this pull request Jul 15, 2015
New API for package log (post-GopherCon)

Fixes #63 and some usability concerns.
@ChrisHines ChrisHines merged commit 8edd386 into master Jul 15, 2015
3 checks passed
3 checks passed
ci/circleci Your tests passed on CircleCI!
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
import "github.com/go-kit/kit/log"

// Levels provides a leveled logging wrapper around a logger. It has five
// levels: debug, info, warning (warn), error, and critical (crit). If you

This comment has been minimized.

@sasha-s

sasha-s Jul 15, 2015

The way log levels are usually (in my practice) used:

  • there is a way to suppress levels up to a minimum severity
  • there is a way to configure a log sink for each log level (so that critical ones a teed to some alerting mechanism for example)

How does it work with levels.Levels?

This comment has been minimized.

@peterbourgon

peterbourgon Jul 15, 2015
Author Member

This Levels type is very basic and doesn't support any of those use cases directly. To get them, you'd need to implement a new Levels type, which I think would be straightforward. If you think your usage generalizes to a lot of other organizations, I'd be happy to review a PR to add it as an additional type in package levels!

This comment has been minimized.

@ChrisHines

ChrisHines Jul 15, 2015
Member

I was expecting we would add support for something along those lines in the future. Before we cram it all into levels.Levels though, consider if it can be achieved with a level aware log.Logger.

This comment has been minimized.

@sasha-s

sasha-s Jul 15, 2015

I am not sure how to do it with the current implementation of log levels.
I would need to extract current log level from the context so I can switch on it etc.
Also, what is the semantics for setting log level multiple times?
(in case of JSON sink the last one wins, if it is a regular loggger they are all printed).

This comment has been minimized.

@peterbourgon

peterbourgon Jul 15, 2015
Author Member

If I understand you correctly, you wouldn't introspect the keyvals for a level key. Rather, you'd construct each leveled logger so that it's connected to the appropriate sink, and enabled/disabled according to the configured minimum severity.

If you supply multiple level keys, they will stack. Keyvals deliberately preserve order and don't dedupe.

This comment has been minimized.

@ChrisHines

ChrisHines Jul 15, 2015
Member

@sasha-s Since this PR has already been merged I recommend that you write up the features you want as separate issues and we'll iterate on them.

@peterbourgon peterbourgon deleted the gophercon-log-api branch Aug 2, 2015
guycook pushed a commit to codelingo/kit that referenced this pull request Oct 12, 2016
New API for package log (post-GopherCon)

Fixes go-kit#63 and some usability concerns.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

5 participants
You can’t perform that action at this time.