log15
log15 copied to clipboard
Make the Handler and Format interfaces more orthogonal.
Context
We have noted the overlap of the Handler and Format APIs (see #8). Specifically they both specify a single function that takes a single *Record argument. This overlap makes the division of labor between Handlers and Formats murky.
We have also noted that writing a well behaved Format is non-trivial and the existing Formats are difficult or impossible to customize without copying (see #10 and #35).
Proposal
Change the Format interface to
// A Formatter formats log contexts as a sequence of bytes.
type Formatter interface {
// Format formats alternating pairs of string keys and values in ctx and
// returns the result. Implementations are encouraged to handle values
// that implement the encoding.TextMarshaler interface by calling their
// MarshalText method.
Format(ctx []interface{}) []byte
}
Rationale
- Passing a log context to the Format method instead of a *Record forces all decisions about the key/value pairs into the Handler chain and Formatters to focus on a single job—formatting output.
- Encouraging use of the encoding.TextMarshaller interface provides an avenue for applications or Handlers to control the output format of individual values without interfering with the surrounding record encoding. Using a standard interface whenever appropriate is better than defining our own.
Interesting proposal. If we adopted this, it would mean the following:
- Most (maybe all)
Handlerchains would need to end with a newHandlerthat inserted the special keys (level, time, message) into the context Formatis still not composable, so it's still not easy to customize them without copyingFormatcode from the individual implementations
It seems like regardless of whether we adopt this or not, we should extract out all the code which is shared between Formats into Handlers. (Putting the special keys into the context, normalizing values into strings via the TextMarshaller interface, etc). The downside to this is that I suspect it will greatly increase the surface area of log15's API. This means both more APIs to support and it makes it more difficult to use because of the additional choices a developer will see. We could consider putting it into a format sub-package, I suppose.
@inconshreveable: Your comments above are thought provoking. I have several things to say in response, but I am going to break them into two posts. This post is a direct response to your comments. The second post will be a more abstract rethinking of the proposal.
RE: Handler Chains
As it stands now most Handler chains end with one of the Handlers that accepts a Format argument which are FileHandler, NetHandler, or StreamHandler. FileHandler and NetHandler are thin wrappers around StreamHandler which contains the only call to Format.Format within log15.
If we provide a Handler that populates the context with the special keys, then, as a convenience, StreamHandler could add it to the chain just as it currently does for LazyHandler and SyncHandler. However, it may be cleaner to simply put the special keys directly into the context.
RE: Composable Formats
I don't consider it a goal to make Format composable. I am not sure what that buys us. Since Formats convert structured data into unstructured []bytes it seems rather difficult to create chains of them.
As mentioned in the proposal's rationale, Formats would support customization by supporting the encoding.TextMarshaller interface.
@inconshreveable: While considering your comments it struck me that the Format interface is really better thought of as an Encoder along the lines of json.Encoder, gob.Encoder, and xml.Encoder. This name convey the idea that the output should be machine readable and that individual values may implement the encoding.*Marshaller interfaces to customize their layout. A typical logging pipeline would look as follows.
- Logger
- Handler
- Handler
- ....
- Handler
- Encoder -> *Marshallers
- io.Writer
We would declare Encoder as:
// An Encoder encodes log contexts.
type Encoder interface {
// Encode encodes the alternating pairs of string keys and values in ctx.
// Implementations should check for values that implement the interfaces
// of the standard library's encoding package.
Encode(ctx []interface{}) error
}
In this design the responsibilities are cleanly divided:
- Handler chains produce log contexts ([]interface{} with alternating key/value pairs).
- Encoders control the record format for a log context and provide default value formats.
- Values optionally control their format by implementing encoding.*Marshaller.
We give Encoders more implementation flexibility by not requiring them to return a []byte. Also, a JsonEncoder or XmlEncoder can simply pass values on to a json.Encoder or xml.Encoder and take advantage of those packages's awareness of the encoding.TextMarshaller interface.