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

Reimplement log/experimental_levels #470

Merged
merged 8 commits into from
Mar 3, 2017
Merged

Reimplement log/experimental_levels #470

merged 8 commits into from
Mar 3, 2017

Conversation

ChrisHines
Copy link
Member

@ChrisHines ChrisHines commented Feb 23, 2017

The main focus here is to get away from identifying level keyvals with string matching and use type matching with a bit set for level filtering in the manner demonstrated by @seh in #453.

I am very happy that all of the configuration options could be preserved. I have taken the liberty to simplify the Options API a bit to make it friendlier. The Allowed option was removed in favor of converting all of its helper functions (e.g. AllowInfoAndAbove) into functional Options that can be used directly. I also changed level.New to level.NewFilter because it better expresses its purpose.

The documentation is updated and corrected.

Before updating the implementation I rewrote the benchmarks to be more comprehensive with the use of the new (Go 1.7) sub-benchmark feature. The before and after results of the benchmarks follow.

Updated benchmarks after 6a12b31.

name                                              old time/op    new time/op    delta
/NoContext/Baseline/Nop-8                            434ns ±13%     371ns ± 7%  -14.55%  (p=0.032 n=5+5)
/NoContext/Baseline/Logfmt-8                         796ns ± 1%     828ns ± 1%   +3.99%  (p=0.008 n=5+5)
/NoContext/Baseline/JSON-8                          2.06µs ± 3%    2.06µs ± 2%     ~     (p=0.690 n=5+5)
/NoContext/DisallowedLevel/Nop-8                     422ns ± 6%     368ns ±19%     ~     (p=0.095 n=5+5)
/NoContext/DisallowedLevel/Logfmt-8                  435ns ± 9%     377ns ±12%     ~     (p=0.056 n=5+5)
/NoContext/DisallowedLevel/JSON-8                    422ns ±11%     355ns ± 4%  -15.89%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/Nop-8                        447ns ± 8%     343ns ± 9%  -23.20%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/Logfmt-8                     824ns ± 1%     833ns ± 2%     ~     (p=0.111 n=5+5)
/NoContext/AllowedLevel/JSON-8                      2.07µs ± 3%    2.05µs ± 1%     ~     (p=0.690 n=5+5)
/TimeContext/Baseline/Nop-8                          780ns ± 4%     702ns ± 1%   -9.95%  (p=0.008 n=5+5)
/TimeContext/Baseline/Logfmt-8                      1.46µs ± 2%    1.47µs ± 1%     ~     (p=0.222 n=5+5)
/TimeContext/Baseline/JSON-8                        3.00µs ± 1%    2.99µs ± 4%     ~     (p=0.690 n=5+5)
/TimeContext/DisallowedLevel/Nop-8                   799ns ± 3%     711ns ± 2%  -11.06%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/Logfmt-8                796ns ± 3%     698ns ± 0%  -12.23%  (p=0.000 n=5+4)
/TimeContext/DisallowedLevel/JSON-8                  792ns ± 1%     714ns ± 3%   -9.85%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/Nop-8                      806ns ± 5%     717ns ± 1%  -10.97%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/Logfmt-8                  1.47µs ± 1%    1.48µs ± 2%     ~     (p=0.056 n=5+5)
/TimeContext/AllowedLevel/JSON-8                    2.99µs ± 3%    2.94µs ± 0%     ~     (p=0.206 n=5+5)
/CallerContext/Baseline/Nop-8                       1.03µs ± 3%    0.97µs ± 1%   -6.45%  (p=0.008 n=5+5)
/CallerContext/Baseline/Logfmt-8                    2.31µs ± 1%    2.33µs ± 1%     ~     (p=0.246 n=5+5)
/CallerContext/Baseline/JSON-8                      3.86µs ± 1%    3.88µs ± 1%     ~     (p=0.286 n=5+5)
/CallerContext/DisallowedLevel/Nop-8                1.04µs ± 0%    0.99µs ± 3%   -4.61%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/Logfmt-8             1.08µs ± 6%    0.97µs ± 2%  -10.10%  (p=0.016 n=5+4)
/CallerContext/DisallowedLevel/JSON-8               1.04µs ± 0%    1.14µs ±29%     ~     (p=1.000 n=4+5)
/CallerContext/AllowedLevel/Nop-8                   1.04µs ± 0%    1.06µs ± 4%     ~     (p=0.698 n=4+5)
/CallerContext/AllowedLevel/Logfmt-8                2.38µs ± 4%    2.79µs ±13%  +17.09%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/JSON-8                  4.03µs ± 3%    4.44µs ± 5%  +10.29%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/Nop-8              1.57µs ±10%    1.57µs ± 8%     ~     (p=1.000 n=5+5)
/TimeCallerReqIDContext/Baseline/Logfmt-8           3.41µs ± 3%    3.56µs ± 1%   +4.56%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/JSON-8             5.60µs ± 2%    5.76µs ± 4%     ~     (p=0.056 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/Nop-8       1.50µs ± 1%    1.46µs ± 2%   -2.69%  (p=0.032 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/Logfmt-8    1.52µs ± 2%    1.46µs ± 1%   -3.86%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/JSON-8      1.50µs ± 1%    1.46µs ± 2%   -2.67%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/Nop-8          1.51µs ± 1%    1.46µs ± 1%   -3.11%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/Logfmt-8       3.45µs ± 3%    3.49µs ± 1%     ~     (p=0.151 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/JSON-8         5.55µs ± 2%    5.80µs ± 2%   +4.42%  (p=0.008 n=5+5)

name                                              old alloc/op   new alloc/op   delta
/NoContext/Baseline/Nop-8                             288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/Baseline/Logfmt-8                          288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/Baseline/JSON-8                          1.06kB ± 0%    1.05kB ± 0%   -1.50%  (p=0.008 n=5+5)
/NoContext/DisallowedLevel/Nop-8                      288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/DisallowedLevel/Logfmt-8                   288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/DisallowedLevel/JSON-8                     288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/Nop-8                         288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/Logfmt-8                      288B ± 0%      256B ± 0%  -11.11%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/JSON-8                      1.06kB ± 0%    1.05kB ± 0%   -1.50%  (p=0.008 n=5+5)
/TimeContext/Baseline/Nop-8                           384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/Baseline/Logfmt-8                        384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/Baseline/JSON-8                        1.28kB ± 0%    1.26kB ± 0%   -1.25%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/Nop-8                    384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/Logfmt-8                 384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/JSON-8                   384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/Nop-8                       384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/Logfmt-8                    384B ± 0%      352B ± 0%   -8.33%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/JSON-8                    1.28kB ± 0%    1.26kB ± 0%   -1.25%  (p=0.008 n=5+5)
/CallerContext/Baseline/Nop-8                         352B ± 0%      320B ± 0%   -9.09%  (p=0.008 n=5+5)
/CallerContext/Baseline/Logfmt-8                      520B ± 0%      488B ± 0%   -6.15%  (p=0.008 n=5+5)
/CallerContext/Baseline/JSON-8                      1.42kB ± 0%    1.40kB ± 0%   -1.13%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/Nop-8                  352B ± 0%      320B ± 0%   -9.09%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/Logfmt-8               352B ± 0%      320B ± 0%   -9.09%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/JSON-8                 352B ± 0%      320B ± 0%   -9.09%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/Nop-8                     352B ± 0%      320B ± 0%   -9.09%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/Logfmt-8                  520B ± 0%      488B ± 0%   -6.15%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/JSON-8                  1.42kB ± 0%    1.40kB ± 0%   -1.13%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/Nop-8                592B ± 0%      560B ± 0%   -5.41%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/Logfmt-8             768B ± 0%      736B ± 0%   -4.17%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/JSON-8             1.86kB ± 0%    1.85kB ± 0%   -0.86%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/Nop-8         592B ± 0%      560B ± 0%   -5.41%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/Logfmt-8      592B ± 0%      560B ± 0%   -5.41%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/JSON-8        592B ± 0%      560B ± 0%   -5.41%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/Nop-8            592B ± 0%      560B ± 0%   -5.41%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/Logfmt-8         768B ± 0%      736B ± 0%   -4.17%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/JSON-8         1.86kB ± 0%    1.85kB ± 0%   -0.86%  (p=0.008 n=5+5)

name                                              old allocs/op  new allocs/op  delta
/NoContext/Baseline/Nop-8                             9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/Baseline/Logfmt-8                          9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/Baseline/JSON-8                            24.0 ± 0%      23.0 ± 0%   -4.17%  (p=0.008 n=5+5)
/NoContext/DisallowedLevel/Nop-8                      9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/DisallowedLevel/Logfmt-8                   9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/DisallowedLevel/JSON-8                     9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/Nop-8                         9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/Logfmt-8                      9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/NoContext/AllowedLevel/JSON-8                        24.0 ± 0%      23.0 ± 0%   -4.17%  (p=0.008 n=5+5)
/TimeContext/Baseline/Nop-8                           10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/Baseline/Logfmt-8                        10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/Baseline/JSON-8                          28.0 ± 0%      27.0 ± 0%   -3.57%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/Nop-8                    10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/Logfmt-8                 10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/DisallowedLevel/JSON-8                   10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/Nop-8                       10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/Logfmt-8                    10.0 ± 0%       8.0 ± 0%  -20.00%  (p=0.008 n=5+5)
/TimeContext/AllowedLevel/JSON-8                      28.0 ± 0%      27.0 ± 0%   -3.57%  (p=0.008 n=5+5)
/CallerContext/Baseline/Nop-8                         9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/CallerContext/Baseline/Logfmt-8                      13.0 ± 0%      11.0 ± 0%  -15.38%  (p=0.008 n=5+5)
/CallerContext/Baseline/JSON-8                        31.0 ± 0%      30.0 ± 0%   -3.23%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/Nop-8                  9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/Logfmt-8               9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/CallerContext/DisallowedLevel/JSON-8                 9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/Nop-8                     9.00 ± 0%      7.00 ± 0%  -22.22%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/Logfmt-8                  13.0 ± 0%      11.0 ± 0%  -15.38%  (p=0.008 n=5+5)
/CallerContext/AllowedLevel/JSON-8                    31.0 ± 0%      30.0 ± 0%   -3.23%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/Nop-8                11.0 ± 0%       9.0 ± 0%  -18.18%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/Logfmt-8             16.0 ± 0%      14.0 ± 0%  -12.50%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/Baseline/JSON-8               39.0 ± 0%      38.0 ± 0%   -2.56%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/Nop-8         11.0 ± 0%       9.0 ± 0%  -18.18%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/Logfmt-8      11.0 ± 0%       9.0 ± 0%  -18.18%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/DisallowedLevel/JSON-8        11.0 ± 0%       9.0 ± 0%  -18.18%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/Nop-8            11.0 ± 0%       9.0 ± 0%  -18.18%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/Logfmt-8         16.0 ± 0%      14.0 ± 0%  -12.50%  (p=0.008 n=5+5)
/TimeCallerReqIDContext/AllowedLevel/JSON-8           39.0 ± 0%      38.0 ± 0%   -2.56%  (p=0.008 n=5+5)

// Info, Warn or Error helper methods are squelched and non-leveled log
// events are passed to next unmodified.
func NewFilter(next log.Logger, options ...Option) log.Logger {
l := &logger{
Copy link
Contributor

Choose a reason for hiding this comment

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

Any technical reason for moving the pointer here instead of passing it as option(&l)?
After some discussions with @goinggo I've taken to passing with & because it implies the semantic meaning of "I'm sharing this to be modified"

Just interested in the discussion on readability and performance with the declarations here :)

Copy link
Member Author

@ChrisHines ChrisHines Feb 23, 2017

Choose a reason for hiding this comment

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

I don't think there is any technical reason. The reason I moved it is because type logger has a pointer receiver on its Log method so it must be a pointer in the returned log.Logger interface value to satisfy the interface. I prefer to create it in the same form that it will live its life, as a *logger.

Copy link
Member

@peterbourgon peterbourgon left a comment

Choose a reason for hiding this comment

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

Very happy with the alloc benchmarks! One API suggestion follows.

}
})
}
})
Copy link
Member

Choose a reason for hiding this comment

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

🏄

logger := tc.format(&buf)
level.Info(logger).Log("foo", "bar")
if want, have := tc.output, strings.TrimSpace(buf.String()); want != have {
t.Errorf("\nwant: '%s'\nhave '%s'", want, have)
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, what does %q yield on Windows?

Copy link
Member Author

Choose a reason for hiding this comment

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

%q works the same on all platforms as it is geared towards escaping strings like you would see in Go source code. It is often unsatisfactory when printing Windows file paths because the \s get doubled up, but that's not my motivation here. In this case the motivation is that I added a test that outputs JSON and didn't want to see lots of \" in the output.

IMO, %q should be used when the intent is for escaped strings. If you just want to demarcate the beginning and ending of a string (to spot extra white space or spot empty strings) then explicit markers are preferred.

Of course now I am thinking that perhaps ` would be better than ' for Go because of its use within the language for unescaped string literals.

// AllowDebugAndAbove allows all of the four default log levels.
func AllowDebugAndAbove() Option {
return allowed(levelDebug | levelInfo | levelWarn | levelError)
}
Copy link
Member

Choose a reason for hiding this comment

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

Looking at this through the fresh eyes of someone reviewing the Proposal doc, I'm raising my eyebrow a little bit at the AllowFooAndAbove nomenclature, specifically the AndAbove bit, which seems necessarily implicit. Therefore the bitfield implementation here, which allows arbitrary selection of levels, may be needless. What do you think about this patch?

Copy link
Contributor

Choose a reason for hiding this comment

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

@peterbourgon, that's the inverse of my earlier commit in #453 ("Use a mask to filter log records by level"). I started with the inequality that you propose here, but found that the bitmask implementation beat it in my benchmarks. Unfortunately, I didn't include the improvement in the commit message, but I recall it being on the order of 3%. It's worth testing again.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. My philosophy tends to be that any code change that deviates from a naive, intuitive implementation needs a degree of justification proportionate to that deviation. There is a lot of subjectivity built in to that criteria, unfortunately. To my subjective eyes, bitmasking levels in this way is enough of a headscratcher that I'd want to see a much clearer win than a 3% performance improvement. With that objection raised, I defer to Chris' final call.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that thinking of the levels as being ordered significantly and comparing via inequality is more intuitive. My goal in #453 was to "build a race car," so for each operation I was trying to find the fastest way to achieve the same functional outcome.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would love to see the exported function names get less verbose. In particular I think the current AndAbove terminology is not always intuitive because I often think of level error as more critical than level info and therefore above it in some way. I am not sure that everyone has the same intuition about which levels are the "high" levels and which are the "low" levels. I've seen both philosophies implemented in various logging libraries.

I do think it is generally accepted that level info is a superset of level warn which is a superset of level error from an output perspective. I think that intuition is what our exported API should lean on.

I am not too concerned about the implementation details. I am fine either way. If we can measure a consistent performance difference I would be inclined to go with the faster implementation along with an appropriate level of justification in internal comments.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm relieved to see another acknowledgment that the order of these common levels is inconsistent in different libraries. I recall disagreeing with log15's ordering, which puts "critical" at the bottom/lowest and "debug" at the top/highest. "Severity" is an ordering that people will understand, but using function names like "InfoAndMoreSevere" didn't ring well to my ear.

Copy link
Member

Choose a reason for hiding this comment

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

Great, so shall we leave the bitmasking as-is, and rename the options?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, let's focus on the exported names and API for now. I think the option names can be improved.

@peterbourgon
Copy link
Member

While working on the proposal, it struck me that a great feature for NewFilter would be an option to promote unleveled log records to leveled, at a chosen level, on an opt-in basis. What do you think?

@ChrisHines
Copy link
Member Author

a great feature for NewFilter would be an option to promote unleveled log records to leveled

That might be useful to some people, but I think it should be implemented as a new Logger rather than an additional option on NewFilter. It is a different kind of functionality and orthogonal to what NewFilter does. NewFilter controls whether a log event is removed from the stream. This new idea is about modifying a log event by injecting a level keyval that doesn't exist.

Outline of one approach

My first idea was to export the level values and provide the following function.

func NewInjector(logger log.Logger, level *levelValue) log.Logger

Which actually works even though levelValue is not exported, but it looks really weird. After futzing around a bit more I arrived at the following structure.

func NewInjector(next log.Logger, level Value) log.Logger

type Value interface {
	String() string
	levelVal()
}

func Key() interface{}  { return key }
func ErrorValue() Value { return errorValue }
func WarnValue() Value  { return warnValue }
func InfoValue() Value  { return infoValue }
func DebugValue() Value { return debugValue }

var (
	key        interface{} = "level"
	errorValue             = &levelValue{level: levelError, name: "error"}
	warnValue              = &levelValue{level: levelWarn, name: "warn"}
	infoValue              = &levelValue{level: levelInfo, name: "info"}
	debugValue             = &levelValue{level: levelDebug, name: "debug"}
)

type levelValue struct {
	name string
	level
}

func (v *levelValue) String() string { return v.name }
func (v *levelValue) levelVal()      {}

Providing the Key and [Level]Value functions gives other packages access to the special values in a read only manner. This is valuable to users that want to write their own level filtering or injection logic. It gives an application that needs to modify the levels emitted by some library according to some specific rule the tools it needs to look for and assign level keyvals.

An application would use NewInjector as follows.

var logger log.Logger
logger = log.NewLogfmtLogger(os.Stdout)
logger = level.NewInjector(logger, level.InfoValue())

I have this coded up with a unit test but no docs yet. Let me know if this looks worth adding and I'll finish it up and add it to this PR.

@peterbourgon
Copy link
Member

Brilliant. Looks great to me. The levelVal method is just to distinguish the interface from e.g. fmt.Stringer, is that correct? And why are func Key() and type key of type interface{} versus e.g. string?

@seh
Copy link
Contributor

seh commented Feb 25, 2017

I'm leery of the utility in exposing the level values like this, even behind these functions.

Though it sounds like you did consider an alternative like this, I'll describe it just to solicit your reaction. What if instead of exporting these six new symbols (a type and five functions), you instead exported four functions like this:

  • func WithDebugAsDefault(log.Logger) log.Logger
  • func WithInfoAsDefault(log.Logger) log.Logger
  • func WithWarnAsDefault(log.Logger) log.Logger
  • func WithErrorAsDefault(log.Logger) log.Logger

Each fixes NewInjector's second parameter into the name of the function, and we avoid exposing the level values to callers.

We use the word "default" all the time in programming, but here it feels especially apt: for lack of a specified alternative, we use this value.

@peterbourgon
Copy link
Member

peterbourgon commented Feb 25, 2017

Why are you leery of exposing level values? I think it's great: it means users may implement their own identical decorators, or even fall back to a more explicit behavior. If we can explain that

level.Debug(logger).Log("msg", "hello")

is just sugar for

logger.Log(level.Key(), level.DebugValue(), "msg", "hello")

then the package gets a lot more understandable, IMO.

@seh
Copy link
Contributor

seh commented Feb 25, 2017

Doing so allows use of the level value in a key position, which one could emulate well enough with a bare string key, but it seems like the filtering Logger might have to contend with more weird situations pertaining to these values when the package doesn't have complete control over their insertion.

It's no worse than it was with strings as the keys and values, but with the private type we have the opportunity to constrain its use. It sounds like you see opening it back up more as an opportunity than a liability. I don't have any real examples of potential misuse here. That's why I said "leery," as opposed to "vehemently opposed." The threat I described over in #453 is more likely to arise.

@ChrisHines
Copy link
Member Author

I am not sympathetic to the concerns about misuse. This package provides convenient helpers that ensure the usage is correct. Code that chooses to use the individual values must also assume the responsibility of doing so correctly.

There is little we can do about code using string values that match the results of the String method on the levelValues. But it does raise an interesting question about what defines a leveled log record, the typed data or the final []byte representation.

I am also not sure how I feel about the fact that this PR will require packages to use its types in order to be compatible with its filter. That is not the case with the existing string based approach. Having said that, exporting the Key and *Value functions opens the door for people to write adapters for other logging packages (e.g. logrus, log15) or packages that use our log.Logger interface but some other means of adding level keyvals. The adapters can inspect the incoming data and convert it to this package's representation.

@ChrisHines
Copy link
Member Author

And why are func Key() and type key of type interface{} versus e.g. string?

That is where one of the allocations is saved. Preallocating those as interface values avoids an allocation per Log call. If they were strings then the compiler would add a heap allocation each time it gets converted to an interface{}. Passing a variable that is already an interface type, or some pointer type, to an interface{} parameter only copies the interface header, and doesn't need to copy the wrapped value and so doesn't need to allocate space for it.

Granted it does make the return type of Key look odd.

@seh
Copy link
Contributor

seh commented Feb 25, 2017

Granted it does make the return type of Key look odd.

You could convert it back and return a string from Key, though. That is, use the interface{} variables internally, but return a string to callers who want the key. Though that yields a less peculiar interface, it then forces each caller to pay a subtle price until they discover the optimization that we are using here internally.

@peterbourgon
Copy link
Member

peterbourgon commented Feb 26, 2017

Preallocating those as interface values avoids an allocation per Log call.

Perfect. Perhaps a comment to this effect?

You could convert it back and return a string from Key, though.

No need.

@peterbourgon peterbourgon mentioned this pull request Feb 26, 2017
6 tasks
@ChrisHines
Copy link
Member Author

Ready for review.

/cc @peterbourgon @seh

type Option func(*logger)
// NewInjector wraps next and returns a logger that adds a Key/level pair to
// the beginning of log events that don't already contain a level.
func NewInjector(next log.Logger, level Value) log.Logger {
Copy link
Member

Choose a reason for hiding this comment

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

"In effect, this gives a default level to logs without a level key."

@peterbourgon
Copy link
Member

With that small addition to the comment on NewInjector, :shipit:

@ChrisHines ChrisHines merged commit 694f6b6 into master Mar 3, 2017
@ChrisHines ChrisHines deleted the log-levels branch March 6, 2017 23:12
jamesgist pushed a commit to jamesgist/kit that referenced this pull request Nov 1, 2024
Reimplement log/experimental_levels
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.

4 participants