-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
net/http/httptrace: add ServerTrace hooks #18997
Comments
These exist apparently, but why? Code should know what it did previously. |
@bradfitz The example I posted is a simplified instance: you create a wrapper that handles logging of all requests and then the individual handlers don't need to do or understand logging themselves. They are free to just call Other things that might get logged there are the URL/path, the duration of the call, etc. It would be a pain to have to do that at essentially every exit site of every middleware or handler. I think it's also a valid separation of concerns. |
Another example beyond logging is collection of metrics. You need to know when you serve 5xx errors, for instance. Again, you don't necessarily want to be doing this everywhere it happens (and in the case of panics, I'm not sure offhand it's possible.) |
Rather than a dozen optional methods, then, I'd prefer to see something like a |
CL https://golang.org/cl/36647 mentions this issue. |
Given that the primary target of this feature is logging, I suggest that the length of the response body would be a good addition to the output of State() |
What if we just provide one canonical ResponseWriter wrapper instead? And if this is about logging, we should (re-)consider the HTTP logging issues/proposals. |
@bradfitz the thing that worries me about providing a wrapper type is also wrapping the other interfaces that may optionally be supported by the underlying type. For instance, http.Pusher is something that's only supported by the http2 ResponseWriters. This wrapper type either has to eschew support for Pusher or it has to "lie" about supporting it. |
I've written a wrapper type like this as well, except instead of just logging the response code, my wrapper type also logged the number of response body bytes written, an md5sum of the response body, the time between the first and last Write call, and a few other miscellaneous things. Skimming through the examples in this comment, I see that I'm not alone -- many of the wrappers provide more than just the response code. I worry that adding types like @joeshaw, have you considered designing a server-side version of httptrace? We considered this at one time, but never worked through the details. Also see #18095 and #3344.
It is convenient to put all logging code in one place. One way to do this is by wrapping the |
@tombergan i hadn't considered an httptrace-equivalent, but perhaps that is a better way to go in order to support different use cases like hashing response bodies. |
CL https://golang.org/cl/37010 mentions this issue. |
I just uploaded a CL that has the very start of this by introducing an httputil.ServerTrace struct. At the moment it only implements a I expect to add StartHandler and EndHandler hooks as well. Since it's implemented in the server, it could contain connection-level hooks as well. One things that's a little strange about it is that the data isn't accessible from the handlers, which I imagine could be a problem if logging code wanted to include additional request-specific data. One possibility might be to pass the |
I think you're right that the server has both connection-level events and request-level events, and those may need to be handled separately. We could start by supporting request-level events only, since that seems to be what is wanted the most. A nice thing about ClientTrace is that it's easy to create a new ClientTrace struct for each request. For example, here's how I use ClientTrace: info := &reqInfo{}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
GotConn: info.GotConn,
...
}))
transport.RoundTrip(req)
...
log(info) This lets me accumulate all info about a single request into a single struct ( packge http
type Server struct {
...
NewTrace func() *httptrace.ServerTrace // optional callback to create a trace for a new request
}
That would work. Another idea is to add an opaquely-typed field to ServerTrace that can be set by NewTrace. Perhaps |
@joeshaw It is theoretically possible to dynamically decide, whether to wrap pusher or not:
The trouble is a) the combinatoric explosion. You have to provide a branch for every combination of interface that could exist. And b) that the wrapping code must know about all possible optional interfaces. You can get around these by providing generic wrapping-code, but that also only solves the issue for all the optional interfaces in net/http itself, not potential third-party packages using the same trick. This is why I'm so troubled by "optional interfaces" and the fact that they are used in net/http so pervasively. It's a very leaky abstraction and sets a bad example. |
@Merovius wow! that is a very interesting hack, and one i hadn't ever considered. I agree that it's not scalable, but thanks for pointing that out. |
Putting things on the context might be interesting. I don't know whether that crosses the line of acceptable uses of context, though. I'm fine with it, but I think others might balk. I think it's better than more optional interfaces at least. |
Context makes some sense for client tracing, in that the client causes it to trace all requests being made, but that's arguably OK. On the server, though, you want to scope it to one request, not all the requests being handled by the server. It's unclear that context can be made to work here. It sounds like there's not a concrete proposal here at this point and that someone needs to write a design doc that incorporates the feedback and feature discussion above. Or we can move this issue out of the proposal process and leave it open as a regular issue until someone does want to write a proposal. |
I think it makes sense to move it out of the proposal process for now, since my original proposal wasn't well-liked. Using |
@rhysh This is an interesting proposal. There are a few things I would tweak about it, like returning interface values for the individual items in the There are a few things I like about it:
The things I am not sure how I feel about:
if p, ok := rw.(http.Pusher); ok {
p.Push(...)
} which is a very standard, idiomatic thing to do. (It would still succeed much of the time, since the underlying private Instead, you have to do: if ro, ok := rw.(http.ResponseOptionser); ok {
if ro.ResponseOptions().Pusher != nil {
ro.ResponseOptions().Pusher.Push(...)
}
} which is a little longer and less natural. Back to the proliferation of It's likely impossible to eliminate the need for This counterproposal doesn't address the proliferation of wrappers, whereas the Unless people are vehemently opposed to this, I may take a stab at it. |
Back to the To see @pteichman's and my progress on |
An alternative would be, to enable reflect to dynamically create methods (via #15924 and #16522). That way, people who want to create wrappers with optional interfaces could do so, e.g. by
Not terribly ergonomic (and I papered over the detail of how |
Stepping back a bit, the reason we originally added There is also #16220, which was not very well motivated in isolation, but would be fixed as a side-effect of adding There is also #20956, which is yet another example of people trying to get at the underlying
@rhysh, can you say more about this? It sounds like you're currently unable to do something that you'd like to do as a result of the Context change.
I am strongly opposed to this. There is no way the standard library will cover all kinds of stats that every user wants to track. Rather than getting ourselves into the business of tracking stats, we should provide APIs that allow users to track whatever stats they need. |
On the contrary: Many of the features that had to be implemented from scratch by that internal package are now done by the standard library. For instance it used to use http.CloseNotifier to learn that the client had disconnected prematurely; as of Go 1.8, the request-scoped Context now provides that signal. As of Go 1.8, Context in net/http has been working well. The focus of the internal package has narrowed, thanks to improvements in the standard library.
The partial overlap I've encountered is that my internal package would additionally wrap the request body to learn when the full request had been written. This forced Because there's so little overlap, what are the problem statements that this issue should address? Here are some possibilities:
The first three seem like they'd be addressed well with a server-side equivalent to the current |
That is a nice summary. I think all six of your listed use cases could be handled by |
In the current implementation draft of The Should I think it would be helpful to split the discussion into 1) information present in the internals of net/http that is not exposed to users, and 2) information that users have, but need better access to. Use of the Since it's attached to the I have a proposal for an alternate design, if we split the functionality more or less into the two types of data I identified above:
(The function wouldn't return a |
Not sure I followed? The hooks will be available via s := &http.Server{
UpdateRequestContext: func(ctx context.Context) context.Context {
sh := newServerHooks() // a user-defined struct
ctx = context.WithValue(ctx, key, sh)
return httptrace.WithServerTrace(ctx, &httptrace.ServerTrace{
GotRequest: sh.GotRequest,
...
})
} The handler can then use
The middleware needs some entry point in order to apply its
Can you explain this with a pseudocode example? I did not follow.
That is a useful distinction for this discussion, however, my main concern is API clutter. I would like to minimize the API surface and avoid cluttering http.Server with callback fields, if possible. I fear we are heading towards API clutter unless the new functions/callbacks/fields we add are sufficiently general. I am promoting two APIs: (1) Wrapping http.ResponseWriter. This will be the canonical way to affect how the response is produced. (2) UpdateRequestContext. This will be the canonical way to get a read-only view of each request for logging. There may need to be a long list of read-only hooks, but at least those hooks will be hidden in net/http/httptrace rather than cluttered in net/http. |
Considering the case where a user wants to log the status code of all responses, here are three possible approaches:
The shortcoming of (1) is that it blocks access to additional methods on the Code for each follows. Does example 2 match the API design you have in mind? Can you give a more concise demonstration of correct use of the new field?
|
If I'm reading this correctly, ServerTrace proposal doesn't allow modification of the response body prior to writing. Therefore it will not allow things like compressing the response body prior to it being sent, for example. Presumably, the ServerTrace struct will need to define another handler: Such a handler would need to receive the body before it's being written, as well as the headers, if there's a need to modify them. And it should return the new body for actual writing. I assume that the request headers are already somehow available from within that handler already, since they also usually play a part here. |
You make a good point @urandom , that packages like https://github.com/NYTimes/gziphandler are currently implemented by wrapping the ResponseWriter with special behavior for Write and Flush. If packages like that one continue to wrap the ResponseWriter, new features exposed as methods will continue to be shadowed. That package also has special behavior for the Push method: it ensures that the synthetic request will have the "Accept-Encoding: gzip" header. And, the handler disables connection hijacking. It needs to intercept several methods in order to change their behavior. Will the ServerTrace struct expand to allow modifying all additional ResponseWriter methods? When new behaviors are added to the ResponseWriter, will they be added to the ServerTrace struct as well? That seems like a lot of API duplication. |
Looks like this didn't happen before the Go 1.10 freeze. Bumping to Go 1.11. |
Another data point of a package which has to deal with the combinatorial explosion of optional interfaces: |
I have an alternative proposal for avoiding the combinatorial explosion of optional interfaces. It is inspired by the causer interface from github.com/pkg/errors ( type Wrapper interface {
Wrap() http.ResponseWriter
} Then for example, if we want to get at the func GetHijacker(w http.ResponseWriter) http.Hijacker {
for w != nil {
if h, ok := w.(http.Hijacker); ok {
return h
}
wrapper, ok := w.(http.Wrapper)
if !ok {
break
}
w = wrapper.Wrap()
}
return nil
} This doesn't solve the problem of getting at additional server event data that |
Problem
A very common pattern in Go HTTP servers is to implement an
http.ResponseWriter
that wraps anotherhttp.ResponseWriter
and captures the status code. This is often used for logging and metrics collection.For example,
I've written something like this a bunch of times. You can find examples in nearly every Go web framework out there. One example is https://github.com/urfave/negroni/blob/master/response_writer.go#L13-L26.
There are some issues with this approach. For instance, my
statusCaptureWriter
doesn't implement other interfaces likehttp.Flusher
,http.CloseNotifier
orhttp.Pusher
. I can't determine at compile time whether the underlyinghttp.ResponseWriter
implementation implements any of these interfaces, so if I choose to implement them I might lie to callers at higher levels of the stack and inadvertently break things. (This particularly a problem with CloseNotifier.)Proposal (rejected, see below)
I'd like to propose an additional interface,
http.Statuser
(better name welcome) that exposes the status code within ahttp.ResponseWriter
implementation. The internalhttp.(*response)
implementation already tracks the status code written, so this can just be exposed and it will automatically implement this interface.Software could avoid wrapping the
http.ResponseWriter
by instead type asserting it tohttp.Statuser
and getting the status as needed there. (And it could optionally continue to wrap the ResponseWriter as needed until this is widely deployed.)Alternative proposal
Implement an
httptrace.ServerTrace
struct that is analogous to theClientTrace
already there. See #18997 (comment) for more info.The text was updated successfully, but these errors were encountered: