-
Notifications
You must be signed in to change notification settings - Fork 2
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
Implement json formatting and static fields #5
Conversation
// New creates a new logger instance. | ||
// DEPRECATED: use `NewLogger(...)` instead. That one returns an interface, | ||
// which allows the underlying data structure to change without breaking | ||
// clients. | ||
func New() *Logger { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we move the NewLogger
code into here instead? It is idiomatic to have the constructor simply be New
and omit the redundant package name in the construction function name.
I know that it's a breaking change but this seems pretty fundamental to how we're using this logger library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason I hesitated is that the old functions return a *Logger
, which is a struct. The new ones return an interface, so in the future we can make changes more safely. Also the new constructor takes different args. Is it ok to break the package like that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm feeling ok with it – if other's aren't, then I'm not married to it.
Ultimately, the lack of versioning in Go is what's failing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we break it how about tagging the current commit with v1.0 and set this to tag 2.0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about v0.5
and now v1.0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that works too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd take a more radical approach and make Logger
an interface and New()
return that interface. The current struct would then become logger
.
Also, by moving to the interface approach, I'd use the format to create a different type of logger per format, instead of having a single struct handle all possible types of formats, i.e. PlainTextFormat
-> plainTextLogger
struct, JsonFormat
-> jsonLogger
struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we do that at the formatter level. That makes sense too.
I think at this point, it would make sense to decouple the outputting of the message itself into an interface. It would remove a whole swath of This is what I'm thinking: type Formatter interface {
LogMessage(logger *log.Logger, name, level, msg string, staticFields map[string]string, extraFields ...interface{})
} And then this code: if DefaultLogger.format == JsonFormat {
logMessageInJson(DefaultLogger.l, id, "ERROR", description, nil, keysAndValues...)
} else {
logMessage(DefaultLogger.l, id, "ERROR", description, nil, keysAndValues...)
} turns into: DefaultLogger.Formatter.LogMessageInJson(DefaultLogger.l, id, "ERROR", description, nil, keysAndValues...) |
Ooo, I like that! I'll try that out. |
@@ -291,18 +403,35 @@ func (s *Logger) SetTimestampFlags(flags int) { | |||
s.l.SetFlags(flags) | |||
} | |||
|
|||
func (s *Logger) SetField(name string, value interface{}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
💅 pls add a comment, especially around the fact this is static.
What do y'all think of using struct for creating a logger? That way if we want to add options in the future, it doesn't break, cuz same function signature: type Options struct {
ID string
Format LogFormat
StaticFields []interface{}
}
func New(options Options) Logger {
return newLoggerStruct(options.Format, options.ID, options.StaticFields)
} Used by a client like: myLogger := log.New(log.Options{ID: "MINE!", Format: log.JsonFormat}) And even if, for example, the |
I think that makes sense. The only change I would make is |
} else { | ||
// Whether it's explicitly a DefaultFormat, or it's an unrecognized value, | ||
// try to take from env var. | ||
envFormat := os.Getenv("DEFAULT_LOG_ENCODING_FORMAT") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's an env var for a lib, we should prefix it with the project name, e.g. GOLOG_DEFAULT_LOG_FORMAT
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I'd also update the other env vars that aren't following this convention.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, since there won't be multiple logging packages per project (right? :badpokerface:) keeping this un-namedspaced is totally fine. In this case I'd maybe just swap (or even drop) "DEFAULT" and "LOG" to keep all env vars consistently prefixed as "LOG_": "LOG_ENCODING_FORMAT"?
Yeah, the options struct makes sense. |
s.staticArgs[name] = fmt.Sprintf("%v", value) | ||
} | ||
|
||
type logFormatter interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name of this interface is a bit misleading. It appears to be a formatter but it also has side effects (printing the log) which aren't immediately obvious.
This also strikes me as a good chance to use go's first-class treatment of functions by changing this interface to become a function alias.
Unless we rename this to a LogPrinter
, I'd definitely leave the actual printing up to whoever uses the formatter (in this case, the logger).
type Formatter func(id string, level LogLevelName /* etc */) string
func formatAsPlainText(id string, level LogLevelName /* etc */) string {
// ...
}
func formatAsJson(id string, level LogLevelName /* etc */) string {
// ...
}
type logger struct {
formatter Formatter
}
func NewJsonLogger() Logger {
return &logger{formatter: formatAsJson}
}
Something along those lines. Playground example: http://play.golang.org/p/z7jjYpcV6a
Definitely |
Ok, I think I addressed everyone's suggestions. Except that I didn't remove the prefix stuff, cuz I haven't gone through our infrastructure and made sure it wasn't being used. Should I? @bdotdub @biasedbit @kjsteuer |
Btw, the new changes don't break whois. And using the new logger logger := log.New(log.Config{ID: "yay", Format: log.JsonFormat}, "userID", userID)
logger.Debug("Oh noes!")
logger.Info("More context:", "admin", user.IsAdmin) And as Bruno suggested, there's a simpler way too: logger := log.NewDefault()
logger.Debug("I like white bread.") |
SetTimestampFlags(flags int) | ||
SetStaticField(name string, value interface{}) | ||
|
||
logMessage(level LogLevelName, id string, description string, keysAndValues ...interface{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if anyone has pointed this out yet, but this will prevent anyone outside of this package from actually implementing this interface. Intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To sum up what we spoke about in person: we mean this interface to allow us to change the underlying struct without breaking users of the package, not for them to have an interface they can implement. So it's fine that they can't.
As always, if they want to mock our Logger
, they can make their own interface and pass that around, and mock it in tests. Just like we do with libraries we use that don't supply interfaces.
Ok, I think I've addressed all comments. How's everyone feeling about it? |
🚢 |
ID: id, | ||
Level: Level, | ||
// Logger config. Default/unset values for each attribute are safe. | ||
type Config struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Not to come in at the 11th hour with another delay, but it looks like our code coverage went down about 10% on this PR. Would be great to get that back up to 67% or higher! :) |
🎊 🎊 🎊 |
omg omg omg. Ship itttttt! |
Implement json formatting and static fields
JsonFormat
for a logger, to output a json blob per log line. This allows structured data to be extracted much more reliably than regex parsing, which breaks on so many scenarios.DefaultFormat
(default for old, deprecatedNew
fns), and it uses an env variable to choosePlainTextFormat
orJsonFormat
, defaulting to plain text. This allows a user to have local development default to plain text, but on the server use json, for log aggregation.Example of
PlainTextFormat
:Example of
JsonFormat
:@kjsteuer @kevin-cantwell @biasedbit @bdotdub