-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
package log #16
package log #16
Conversation
@ChrisHines some high-level questions first, so I can better understand the context...
|
Good questions @peterbourgon. The overall design follows most closely to log15, so reading its docs will likely help.
I don't know what you mean by a "label dimension", but I will explain my thinking behind Logrus, lager, and log15 all use Having keys that cannot accidentally conflict with keys from other packages allows handlers to confidently operate on the known key and still let apps make the final call on how those keys are emitted to the backend, thus the
Conceptually a Handler filters and routes structured log records in key-value form. Handlers compose with other Handlers to form arbitrarily sophisticated logging pipelines. An Encoder converts a structured log record to unstructured form ( I have gone back and forth on this question myself. I agree that it feels like all we should need are Handlers and package
I'm not sure. I haven't found good documentation for the semantics of |
if err != nil { | ||
return err | ||
} | ||
_, err = w.Write(b) |
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.
this should be protected by a mutex, or some way to serialize the writes
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.
You are correct. I left it out in this early API sketch. We could lock a mutex around the call to w.Write.
Another approach is to give options to the app code by providing a SyncHandler and an AsyncHandler. Both would wrap an arbitrary Handler. SyncHandler protects the wrapped handler via a Mutex. AsyncHandler serializes the pipeline via a (maybe buffered) channel and introduces a goroutine to run the downstream handlers asynchronously.
Given
Thanks for the context. I have a few thoughts here that I'm struggling to put into a coherent narrative, so please forgive me as I just kind of dump them out :)
Perhaps I'm thinking too naïvely, but I expected an API like type Logger interface {
With(Field) Logger
Log(format string, args ...interface{})
}
type Field struct {
Key, Value string
}
I see. Do we actually want "arbitrarily sophisticated logging pipelines" within a single process -- especially encoded into the base type definitions? I know people who'd consider that to be an antipattern. Could those semantics rather be expressed as an opt-in abstraction layer? For example, taking my func Split(next1, next2 Logger) Logger {
return split{next1, next2}
}
func (s split) Log(format string, args ...interface[}) {
s.next1.Log(format, args...)
s.next2.Log(format, args...)
}
func Filter(matchRegex string, next Logger) Logger {
return filter{regexp.MustCompile(matchRegex), next}
}
func (f filter) Log(format string, args ...interface{}) {
if !s.re.Match(fmt.Sprintf(format, args...)) {
return
}
f.next.Log(format, args...)
} Sorry again for my rambling nature, and thanks in advance for your feedback. I'd also like to hear opinions from @taotetek and @tsenart on this... |
I agree that it is important for
I'm not sure if that is a good idea yet either. That is the biggest experiment in this sketch.
Can you explain what you consider the boundaries of the user-domain from gokit's perspective? In the context of the log API I've sketched out, I consider the Handlers and Encoders part of the user-domain because the main program has control over them, injects them into the Loggers via More concretely, Gokit RFC004 says, "Log SHALL include severity as a k/v pair and allow setting it through the same mechanism as any other k/v pair." If an application wants to filter log records based on severity then introspecting the severity value in the k/v set seems the most obvious approach to me (see log15.LvlFilterHandler. Which leads to the problem of finding the severity without having some reserved keywords that callers to l.Log(log.LvlKey, log.Info, "msg", "Hello, world!") Which seems rather verbose, so the introduction of the (admittedly magical) KeyVal interface allows for: l.Log(log.Info, "msg", "Hello, world!") Which isn't so bad, and actually not far from the typical
What would a call sequence equivalent to mine above look like with that API?
I am having trouble parsing the phrase "encoded into the base type definitions" in this context, please explain. The logging pipelines are completely under application control and may do anything from discard every log record, to asynchronously write to a network end point, with fail-over to local disk on error. One of my favorite Handlers from log15 for diagnostic logging is the EscalateErrHandler, which elevates the severity of a log record to Error if any of the k/v pairs has a non- My goal is for a structured log API with composability on par with package
It seems to me that approach combines the duties of the Logger and the Handler. It could work. It may pose a challenge handling a common requirement in diagnostic logging—although I didn't include it in my PR yet—to collect the source file and line number (or the stack trace) for each log record. Once you start wrapping Loggers around each other it becomes tricky to know how many layers up the stack to go before finding the last application stack frame. A thin Logger layer makes it easy to control the stack depth for the calls to |
I meant only that it seemed like a violation of bounded context to have package log introspect on user-provided keyvals. But, as you rightly point out,
creates an opportunity for confusion. The intention there, I believe, was to have contractual enumerations for severity that can be relied upon within gokit itself. But I anticipated that would take the form of var (
SeverityKey = "severity"
SeverityDebug = "debug"
SeverityInfo = "info"
SeverityError = "error"
) and be utilized like func (t type) internalGokitFunction() {
t.logger.With(log.SeverityKey, log.SeverityDebug).Log("something's happening") // or...
t.debug.Log("something's happening") // t.debug being a pre-contextified log.Logger
}
(I believe I answer that with the above code excerpt. Please let me know if anything is still unclear.)
My intuition is that handling should be composed from, rather than injected into, Loggers. You get at my meaning here, too:
Indeed I don't (presently) understand why there should be a distinction between Loggers and Handler. They're both things that implement Log. Maybe you mean to parallel http.ServeMux and http.Handler?
That's fair. I'm biased toward functional composition whenever possible, and prefer to start there and be forced to concede to other methods when necessary. Of course — e.g. w/ threading context.Context through request handlers — sometimes it's necessary. |
I've drafted a quick summary of my ideas about package log over in #21 |
#21 is merged so we'll close this one—thanks for the comments and feedback! |
First sketch of basic logging features.