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

Allow to configure allowed levels by string value #22

Merged
merged 7 commits into from
Apr 27, 2022
Merged

Allow to configure allowed levels by string value #22

merged 7 commits into from
Apr 27, 2022

Conversation

mcosta74
Copy link
Contributor

@mcosta74 mcosta74 commented Mar 27, 2022

This PR allows to configure the allowed log levels from a string.

Closes: #18

Use Case:

Read the allowed log levels reading environment variable

logger = level.NewFilter(logger, level.AllowByString(os.Getenv("LOG_LEVEL"), level.AllowInfo)) // <--

@ChrisHines
Copy link
Member

@mcosta74 Thanks for the well written PR. You added docs, examples, and all the nice touches. Much appreciated.

I'm still not sure if we want to add this API, but if we do I think your approach takes the right approach. So, before I add any comments on the details I want to poll the other maintainers for their opinion.

@peterbourgon, @sagikazarmark what do you think of adding this option?

@sagikazarmark
Copy link
Contributor

I'm not 100% sure why a fallback option is necessary. As far as I can tell, it's equivalent to the following:

logger = level.NewFilter(logger, level.AllowInfo, level.AllowByString(os.Getenv("LOG_LEVEL")))

If the string doesn't match any of the log levels, it should just be noop.

Personally, I'm not a huge fan of fallbacks in this case though. Consider the following: let's say you misconfigure the production instance and as a result debug logs will start flowing in, potentially containing PII without you even noticing.

I usually log a warning or an error whenever the log config is invalid (which is also why I manually parse log levels and match them to the appropriate log levels).

The name AllowByString feels a bit weird. I'd go with something like Parse.

Other than these, I don't mind adding a few utility functions if they really help.

@mcosta74
Copy link
Contributor Author

mcosta74 commented Mar 27, 2022

If the string doesn't match any of the log levels, it should just be noop.

Personally, I'm not a huge fan of fallbacks in this case though. Consider the following: let's say you misconfigure the production instance and as a result debug logs will start flowing in, potentially containing PII without you even noticing.

@sagikazarmark Agree, the fallback mechanism may obfuscate a misconfiguration.

Should I apply the changes or better to wait a final decision about adding or not that function ?

The name AllowByString feels a bit weird. I'd go with something like Parse.

I'm open to change the name if the approach is accepted

@mcosta74
Copy link
Contributor Author

I've applied the changes suggested by @sagikazarmark.
Let me know about the final decision.

level/doc.go Outdated Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
@mcosta74
Copy link
Contributor Author

mcosta74 commented Apr 5, 2022

hello @peterbourgon, I've applied all changes requested.

Proper grammar and spelling, please :) Let me know if you'd like suggestions.

Please let me know if you find better wording for the doc strings. I'm open to change.

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.

This isn't totally complete but I hope my intent is clear :)

level/doc.go Outdated Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
Comment on lines 41 to 60
func Example_parse() {
// Set up logger with level filter.
allow, err := level.ParseOption("info")
if err != nil {
// fallback to default value in case parse failure
allow = level.AllowInfo()
}
logger := log.NewLogfmtLogger(os.Stdout)
logger = level.NewFilter(logger, allow)
logger = log.With(logger, "caller", log.DefaultCaller)

// Use level helpers to log at different levels.
level.Error(logger).Log("err", errors.New("bad data"))
level.Info(logger).Log("event", "data saved")
level.Debug(logger).Log("next item", 17) // filtered

// Output:
// level=error caller=example_test.go:53 err="bad data"
// level=info caller=example_test.go:54 event="data saved"
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
func Example_parse() {
// Set up logger with level filter.
allow, err := level.ParseOption("info")
if err != nil {
// fallback to default value in case parse failure
allow = level.AllowInfo()
}
logger := log.NewLogfmtLogger(os.Stdout)
logger = level.NewFilter(logger, allow)
logger = log.With(logger, "caller", log.DefaultCaller)
// Use level helpers to log at different levels.
level.Error(logger).Log("err", errors.New("bad data"))
level.Info(logger).Log("event", "data saved")
level.Debug(logger).Log("next item", 17) // filtered
// Output:
// level=error caller=example_test.go:53 err="bad data"
// level=info caller=example_test.go:54 event="data saved"
}
func Example_parse() {
fs := flag.NewFlagSet("myprogram", flag.ExitOnError)
lv := fs.String("log", "info", "debug, info, warn, or error")
fs.Parse([]string{"-log", "warn"})
logger := log.NewLogfmtLogger(os.Stdout)
logger = level.NewFilter(logger, level.MustParseLevel(*lv))
level.Error(logger).Log("msg", "alpha")
level.Warn(logger).Log("msg", "beta")
level.Info(logger).Log("msg", "delta")
level.Debug(logger).Log("msg", "kappa")
// Output:
// level=error msg="alpha"
// level=warn msg="beta"
}

@mcosta74
Copy link
Contributor Author

mcosta74 commented Apr 9, 2022 via email

@coveralls
Copy link

coveralls commented Apr 11, 2022

Pull Request Test Coverage Report for Build 2145714702

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 25 of 25 (100.0%) changed or added relevant lines in 1 file are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage increased (+2.5%) to 74.597%

Files with Coverage Reduction New Missed Lines %
level/level.go 1 99.11%
Totals Coverage Status
Change from base Build 2065105133: 2.5%
Covered Lines: 555
Relevant Lines: 744

💛 - Coveralls

@mcosta74
Copy link
Contributor Author

I think I implemented the agreed API, if the code is code we can also squash the commits

@ChrisHines
Copy link
Member

I think I implemented the agreed API, if the code is code we can also squash the commits

Thanks, @mcosta74. I will review the code sometime in the next few days. Also, thanks for you patience during the API debate.

@mcosta74
Copy link
Contributor Author

I think I implemented the agreed API, if the code is code we can also squash the commits

Thanks, @mcosta74. I will review the code sometime in the next few days. Also, thanks for you patience during the API debate.

no problem at all, I think it was a very interesting and productive discussion. I would only suggest to create a CONTRIBUTION page (maybe with a discussion template) to clarify what are the principles to follow in case someone else wants to propose new API.

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.

A few things.

level/level.go Outdated
@@ -66,6 +75,26 @@ func (l *logger) Log(keyvals ...interface{}) error {
// Option sets a parameter for the leveled logger.
type Option func(*logger)

// Allow allows to configure the log level with a Value.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Allow allows to configure the log level with a Value.
// Allow the provided log level to pass.

@@ -12,11 +12,51 @@ import (
)

func TestVariousLevels(t *testing.T) {

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

level/level_test.go Outdated Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
level/level.go Outdated
Comment on lines 153 to 160
// ParseDefault returns a Value from a level string or the default if the level is not valid.
func ParseDefault(level string, def Value) Value {
if v, err := Parse(level); err != nil {
return def
} else {
return v
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// ParseDefault returns a Value from a level string or the default if the level is not valid.
func ParseDefault(level string, def Value) Value {
if v, err := Parse(level); err != nil {
return def
} else {
return v
}
}
// ParseDefault calls Parse, and returns the default Value on error.
func ParseDefault(level string, def Value) Value {
if v, err := Parse(level); err == nil {
return v
}
return def
}

level/level.go Outdated
Comment on lines 132 to 135
// Parse returns a Value from a level string. Allowed values are "debug", "info", "warn" and "error".
// Levels are normalized via strings.TrimSpace and strings.ToLower
func Parse(level string) (Value, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Parse returns a Value from a level string. Allowed values are "debug", "info", "warn" and "error".
// Levels are normalized via strings.TrimSpace and strings.ToLower
func Parse(level string) (Value, error) {
// Parse a string to it's corresponding level value. Valid strings are "debug",
// "info", "warn", and "error". Strings are normalized via strings.TrimSpace and
// strings.ToLower.
func Parse(level string) (Value, error) {

level/doc.go Outdated
Comment on lines 10 to 20
// It's also possible to configure log level from a string (for instance from
// a flag, environment variable or configuration file).
//
// fs := flag.NewFlagSet("example")
// lvl := fs.String("log-level", "", `"debug", "info", "warn" or "error"`)
//
// var logger log.Logger
// logger = log.NewLogfmtLogger(os.Stderr)
// logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.InfoValue()))) // <--
// logger = log.With(logger, "ts", log.DefaultTimestampUTC)
//
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// It's also possible to configure log level from a string (for instance from
// a flag, environment variable or configuration file).
//
// fs := flag.NewFlagSet("example")
// lvl := fs.String("log-level", "", `"debug", "info", "warn" or "error"`)
//
// var logger log.Logger
// logger = log.NewLogfmtLogger(os.Stderr)
// logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.InfoValue()))) // <--
// logger = log.With(logger, "ts", log.DefaultTimestampUTC)
//
// It's also possible to configure log level from a string. For instance, from
// a flag, environment variable or configuration file.
//
// fs := flag.NewFlagSet("myprogram")
// lvl := fs.String("log", "info", "debug, info, warn, error")
//
// var logger log.Logger
// logger = log.NewLogfmtLogger(os.Stderr)
// logger = level.NewFilter(logger, level.Allow(level.ParseDefault(*lvl, level.InfoValue()))) // <--
// logger = log.With(logger, "ts", log.DefaultTimestampUTC)
//

Copy link
Member

@ChrisHines ChrisHines left a comment

Choose a reason for hiding this comment

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

This is a nice addition to the package. Thanks for the contribution. I have a few nits, mostly style tweaks.

level/level.go Show resolved Hide resolved
level/level.go Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
level/level.go Show resolved Hide resolved
level/level.go Outdated Show resolved Hide resolved
level/level_test.go Outdated Show resolved Hide resolved
// Parse a string to its corresponding level value. Valid strings are "debug",
// "info", "warn", and "error". Strings are normalized via strings.TrimSpace and
// strings.ToLower.
func Parse(level string) (Value, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If the only returned error is invalid level, then maybe the second argument could simply be a boolran?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

isn't more idiomatic to return an error? maybe in the future we might return different types of errors and we'll not need to introduce breaking changes in the API

Copy link
Member

Choose a reason for hiding this comment

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

I agree, error is a bit more future proof and IMO not meaningfully different for callers.

Copy link
Member

@ChrisHines ChrisHines left a comment

Choose a reason for hiding this comment

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

LGTM. 🎉

@ChrisHines
Copy link
Member

@peterbourgon I think we're just waiting for you to sign off on this since you previously requested changes.

Copy link
Contributor

@sagikazarmark sagikazarmark left a comment

Choose a reason for hiding this comment

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

LGTM

level/level.go Outdated Show resolved Hide resolved
Co-authored-by: Márk Sági-Kazár <sagikazarmark@users.noreply.github.com>
@mcosta74
Copy link
Contributor Author

Any update on this ?

@ChrisHines
Copy link
Member

@peterbourgon Everyone involved has approved this PR except you. I think @mcosta74 has addressed all of your feedback. Please verify and approve if you agree. Thanks.

@peterbourgon
Copy link
Member

Oops, sorry! I didn't know I was blocking.

@ChrisHines ChrisHines merged commit 0b69c70 into go-kit:main Apr 27, 2022
@ChrisHines
Copy link
Member

Thanks for the contribution @mcosta74 🎉 .

@mcosta74 mcosta74 deleted the feat/configure-level-with-string branch April 27, 2022 13:59
@mcosta74
Copy link
Contributor Author

Thanks for the contribution @mcosta74 🎉 .

you're welcome

@mcosta74
Copy link
Contributor Author

Hello, are you planning to release a new version of the package ?

@peterbourgon
Copy link
Member

Sure, we can do that — v0.2.1 or v0.3.0? I don't think we made any breaking changes, so my instinct is v0.2.1?

@ChrisHines
Copy link
Member

My vote is v0.2.1 as well. No breaking changes, just a new feature.

@peterbourgon
Copy link
Member

Great. Assuming no objections, I'll tag a release in a day or so.

@mcosta74
Copy link
Contributor Author

not in my hands to make decisions but, since we added a feature following the SemVer guidelines I'd go for a 0.3.0. I'll keep the last number for bug-fixes

@ChrisHines
Copy link
Member

@mcosta74 That is a perfectly correct reading of the SemVer guidelines. But my interpretation is that only strictly applies for major version >= 1.

From semver.org:

  1. Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.

  2. Version 1.0.0 defines the public API. The way in which the version number is incremented after this release is dependent on this public API and how it changes.

So I think it is also correct for us to do whatever we want. I don't want to suggest we should start making lots of breaking changes. I consider this module highly stable, and the API of the core log package extremely stable. We haven't made any breaking changes that I recall in ~5 years and I don't want to start doing so.

But if we stay on major version 0 per #24 (comment), then I think it is fair and appropriate to adapt the y semver number for "biggish" changes and the z number for "smallish" changes. I think it is more about signaling to consumers how much they should pay attention before upgrading.

@mcosta74
Copy link
Contributor Author

@ChrisHines Thanks for the explanation. Whit this in mind I agree that 0.2.1 is the right choice

@mcosta74
Copy link
Contributor Author

Hi @peterbourgon any ETA about the release? I'm using the "unreleased" version in a project we're building but I'd like to use a "released" one

@peterbourgon
Copy link
Member

My apologies for the delay. v0.2.1 is now released. Please let me know if there are any issues.

@mcosta74
Copy link
Contributor Author

@peterbourgon, no problem. Thank you again for the products you give to the community.

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.

consider providing a method to compute the log level based on a String
5 participants