This repository has been archived by the owner on Sep 15, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathcapture_middleware.go
138 lines (127 loc) · 4.79 KB
/
capture_middleware.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package http_logrus
import (
"bytes"
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"github.com/improbable-eng/go-httpwares"
"github.com/improbable-eng/go-httpwares/logging"
"github.com/improbable-eng/go-httpwares/logging/logrus/ctxlogrus"
"github.com/sirupsen/logrus"
"net"
)
// ContentCaptureMiddleware is a server-side http ware for logging contents of HTTP requests and responses (body and headers).
//
// Only requests with a set Content-Length will be captured, with no streaming or chunk encoding supported.
// Only responses with Content-Length set are captured, no gzipped, chunk-encoded responses are supported.
//
// The body will be recorded as a separate log message. Body of `application/json` will be captured as
// http.request.body_json (in structured JSON form) and others will be captured as http.request.body_raw logrus field
// (raw base64-encoded value).
//
// This *must* be used together with http_logrus.Middleware, as it relies on the logger provided there. However, you can
// override the `logrus.Entry` that is used for logging, allowing for logging to a separate backend (e.g. a different file).
func ContentCaptureMiddleware(entry *logrus.Entry, decider http_logging.ContentCaptureDeciderFunc) httpwares.Middleware {
return func(nextHandler http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if !decider(req) {
nextHandler.ServeHTTP(resp, req)
return
}
logger := entry.WithFields(ctxlogrus.Extract(req.Context()).Data)
logger.WithFields(defaultRequestFields(req))
if err := captureMiddlewareRequestContent(req, logger); err != nil {
// this is *really* bad, we failed to read a body because of a read error.
resp.WriteHeader(500)
logger.WithError(err).Warningf("error in logrus middleware on body read")
return
}
wrappedResp := httpwares.WrapResponseWriter(resp)
responseCapture := captureMiddlewareResponseContent(wrappedResp, logger)
nextHandler.ServeHTTP(wrappedResp, req)
responseCapture.finish() // captureResponse has a nil check, this can be nil
})
}
}
func headerIsJson(header http.Header) bool {
return strings.HasPrefix(strings.ToLower(header.Get("content-type")), "application/json")
}
func captureMiddlewareRequestContent(req *http.Request, entry *logrus.Entry) error {
if req.ContentLength <= 0 || req.Body == nil {
// -1 value means that the length cannot be determined, and that it is probably a multipart stremaing call
if req.ContentLength != 0 || req.Body == nil {
entry.Infof("request body capture skipped, content length negative")
}
return nil
}
content, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}
// Make sure we give the Response back its body so the client can read it.
req.Body = ioutil.NopCloser(bytes.NewReader(content))
if strings.HasPrefix(strings.ToLower(req.Header.Get("content-type")), "application/json") {
entry.WithField("http.request.body_json", json.RawMessage(content)).Info("request body captured in http.request.body_json field")
} else {
entry.WithField("http.request.body_raw", base64.StdEncoding.EncodeToString(content)).Info("request body captured in http.request.body_raw field")
}
return nil
}
type responseCapture struct {
content bytes.Buffer
isJson bool
entry *logrus.Entry
}
func (c *responseCapture) observeWrite(resp httpwares.WrappedResponseWriter, buf []byte, n int, err error) {
if err == nil {
c.content.Write(buf[:n])
}
}
func (c *responseCapture) finish() {
if c == nil {
return
}
if c.content.Len() == 0 {
return
}
if c.isJson {
e := c.entry.WithField("http.response.body_json", json.RawMessage(c.content.Bytes()))
e.Info("response body captured in http.response.body_json field")
} else {
e := c.entry.WithField("http.response.body_raw", base64.StdEncoding.EncodeToString(c.content.Bytes()))
e.Info("response body captured in http.response.body_raw field")
}
}
func captureMiddlewareResponseContent(w httpwares.WrappedResponseWriter, entry *logrus.Entry) *responseCapture {
c := &responseCapture{entry: entry}
w.ObserveWriteHeader(func(w httpwares.WrappedResponseWriter, code int) {
if te := w.Header().Get("transfer-encoding"); te != "" {
entry.Infof("response body capture skipped, transfer encoding is not identity")
return
}
c.isJson = headerIsJson(w.Header())
w.ObserveWrite(c.observeWrite)
})
return c
}
func defaultRequestFields(req *http.Request) logrus.Fields {
fields := logrus.Fields{}
if addr := req.RemoteAddr; addr != "" {
if strings.Contains(addr, ":") {
if host, port, err := net.SplitHostPort(addr); err == nil {
fields["peer.address"] = host
fields["peer.port"] = port
}
} else {
fields["peer.address"] = addr
}
}
host := req.URL.Host
if host == "" {
host = req.Host
}
fields["http.host"] = host
return fields
}