1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package githubapp
16
17 import (
18 "bytes"
19 "io"
20 "net/http"
21 "regexp"
22 "time"
23
24 "github.com/gregjones/httpcache"
25 "github.com/rs/zerolog"
26 )
27
28
29
30
31
32 func ClientLogging(lvl zerolog.Level, opts ...ClientLoggingOption) ClientMiddleware {
33 var options clientLoggingOptions
34 for _, opt := range opts {
35 opt(&options)
36 }
37
38 return func(next http.RoundTripper) http.RoundTripper {
39 return roundTripperFunc(func(r *http.Request) (*http.Response, error) {
40 var err error
41 var reqBody, resBody []byte
42
43 if requestMatches(r, options.RequestBodyPatterns) {
44 if r, reqBody, err = mirrorRequestBody(r); err != nil {
45 return nil, err
46 }
47 }
48
49 start := time.Now()
50 res, err := next.RoundTrip(r)
51 elapsed := time.Now().Sub(start)
52
53 evt := zerolog.Ctx(r.Context()).
54 WithLevel(lvl).
55 Str("method", r.Method).
56 Str("path", r.URL.String()).
57 Dur("elapsed", elapsed)
58
59 if reqBody != nil {
60 evt.Bytes("request_body", reqBody)
61 }
62
63 if res != nil {
64 cached := res.Header.Get(httpcache.XFromCache) != ""
65 evt.Bool("cached", cached).
66 Int("status", res.StatusCode)
67
68 size := res.ContentLength
69 if requestMatches(r, options.ResponseBodyPatterns) {
70 if res, resBody, err = mirrorResponseBody(res); err != nil {
71 return res, err
72 }
73 if size < 0 {
74 size = int64(len(resBody))
75 }
76 evt.Int64("size", size).Bytes("response_body", resBody)
77 } else {
78 evt.Int64("size", size)
79 }
80 } else {
81 evt.Bool("cached", false).
82 Int("status", -1).
83 Int64("size", -1)
84 }
85
86 evt.Msg("github_request")
87 return res, err
88 })
89 }
90 }
91
92
93 type ClientLoggingOption func(*clientLoggingOptions)
94
95 type clientLoggingOptions struct {
96 RequestBodyPatterns []*regexp.Regexp
97 ResponseBodyPatterns []*regexp.Regexp
98 }
99
100
101
102
103 func LogRequestBody(patterns ...string) ClientLoggingOption {
104 regexps := compileRegexps(patterns)
105 return func(opts *clientLoggingOptions) {
106 opts.RequestBodyPatterns = regexps
107 }
108 }
109
110
111
112
113 func LogResponseBody(patterns ...string) ClientLoggingOption {
114 regexps := compileRegexps(patterns)
115 return func(opts *clientLoggingOptions) {
116 opts.ResponseBodyPatterns = regexps
117 }
118 }
119
120 func mirrorRequestBody(r *http.Request) (*http.Request, []byte, error) {
121 switch {
122 case r.Body == nil || r.Body == http.NoBody:
123 return r, []byte{}, nil
124
125 case r.GetBody != nil:
126 br, err := r.GetBody()
127 if err != nil {
128 return r, nil, err
129 }
130 body, err := io.ReadAll(br)
131 closeBody(br)
132 return r, body, err
133
134 default:
135 body, err := io.ReadAll(r.Body)
136 closeBody(r.Body)
137 if err != nil {
138 return r, nil, err
139 }
140 rCopy := r.Clone(r.Context())
141 rCopy.Body = io.NopCloser(bytes.NewReader(body))
142 return rCopy, body, nil
143 }
144 }
145
146 func mirrorResponseBody(res *http.Response) (*http.Response, []byte, error) {
147 body, err := io.ReadAll(res.Body)
148 closeBody(res.Body)
149 if err != nil {
150 return res, nil, err
151 }
152
153 res.Body = io.NopCloser(bytes.NewReader(body))
154 return res, body, nil
155 }
156
157 func compileRegexps(pats []string) []*regexp.Regexp {
158 regexps := make([]*regexp.Regexp, len(pats))
159 for i, p := range pats {
160 regexps[i] = regexp.MustCompile(p)
161 }
162 return regexps
163 }
164
165 func requestMatches(r *http.Request, pats []*regexp.Regexp) bool {
166 for _, pat := range pats {
167 if pat.MatchString(r.URL.Path) {
168 return true
169 }
170 }
171 return false
172 }
173
174 func closeBody(b io.ReadCloser) {
175 _ = b.Close()
176 }
177
View as plain text