1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
8 // LoggingResponseWriter
16 "git.curoverse.com/arvados.git/sdk/go/httpserver"
17 "git.curoverse.com/arvados.git/sdk/go/stats"
18 log "github.com/Sirupsen/logrus"
21 // LoggingResponseWriter has anonymous fields ResponseWriter and ResponseBody
22 type LoggingResponseWriter struct {
30 // CloseNotify implements http.CloseNotifier.
31 func (resp *LoggingResponseWriter) CloseNotify() <-chan bool {
32 wrapped, ok := resp.ResponseWriter.(http.CloseNotifier)
34 // If upstream doesn't implement CloseNotifier, we can
35 // satisfy the interface by returning a channel that
36 // never sends anything (the interface doesn't
37 // guarantee that anything will ever be sent on the
38 // channel even if the client disconnects).
41 return wrapped.CloseNotify()
44 // WriteHeader writes header to ResponseWriter
45 func (resp *LoggingResponseWriter) WriteHeader(code int) {
46 if resp.sentHdr == zeroTime {
47 resp.sentHdr = time.Now()
50 resp.ResponseWriter.WriteHeader(code)
53 var zeroTime time.Time
55 func (resp *LoggingResponseWriter) Write(data []byte) (int, error) {
56 if resp.Length == 0 && len(data) > 0 && resp.sentHdr == zeroTime {
57 resp.sentHdr = time.Now()
59 resp.Length += len(data)
60 if resp.Status >= 400 {
61 resp.ResponseBody += string(data)
63 return resp.ResponseWriter.Write(data)
66 // LoggingRESTRouter is used to add logging capabilities to mux.Router
67 type LoggingRESTRouter struct {
69 idGenerator httpserver.IDGenerator
72 func (loggingRouter *LoggingRESTRouter) ServeHTTP(wrappedResp http.ResponseWriter, req *http.Request) {
75 // Attach a requestID-aware logger to the request context.
76 lgr := log.WithField("RequestID", loggingRouter.idGenerator.Next())
77 ctx := context.WithValue(req.Context(), "logger", lgr)
78 req = req.WithContext(ctx)
80 lgr = lgr.WithFields(log.Fields{
81 "remoteAddr": req.RemoteAddr,
82 "reqForwardedFor": req.Header.Get("X-Forwarded-For"),
83 "reqMethod": req.Method,
84 "reqPath": req.URL.Path[1:],
85 "reqBytes": req.ContentLength,
89 resp := LoggingResponseWriter{http.StatusOK, 0, wrappedResp, "", zeroTime}
90 loggingRouter.router.ServeHTTP(&resp, req)
93 statusText := http.StatusText(resp.Status)
94 if resp.Status >= 400 {
95 statusText = strings.Replace(resp.ResponseBody, "\n", "", -1)
97 if resp.sentHdr == zeroTime {
98 // Nobody changed status or wrote any data, i.e., we
99 // returned a 200 response with no body.
103 lgr.WithFields(log.Fields{
104 "timeTotal": stats.Duration(tDone.Sub(tStart)),
105 "timeToStatus": stats.Duration(resp.sentHdr.Sub(tStart)),
106 "timeWriteBody": stats.Duration(tDone.Sub(resp.sentHdr)),
107 "respStatusCode": resp.Status,
108 "respStatus": statusText,
109 "respBytes": resp.Length,