Skip to content

Commit e72a5f4

Browse files
committed
Move requestlog from libserv
1 parent 6ef0988 commit e72a5f4

File tree

5 files changed

+223
-0
lines changed

5 files changed

+223
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ require (
1717
github.com/mattn/go-colorable v0.1.13 // indirect
1818
github.com/mattn/go-isatty v0.0.19 // indirect
1919
github.com/pmezard/go-difflib v1.0.0 // indirect
20+
github.com/rs/xid v1.5.0 // indirect
2021
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU
1515
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1616
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1717
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18+
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
1819
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
1920
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
2021
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=

requestlog/accesslogger.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package requestlog
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"time"
8+
9+
"github.com/rs/zerolog"
10+
"github.com/rs/zerolog/hlog"
11+
)
12+
13+
const MaxRequestSizeLog = 4 * 1024
14+
const MaxStringRequestSizeLog = MaxRequestSizeLog / 2
15+
16+
func AccessLogger(logOptions bool) func(http.Handler) http.Handler {
17+
return func(next http.Handler) http.Handler {
18+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
log := hlog.FromRequest(r)
20+
21+
crw := &CountingResponseWriter{
22+
ResponseWriter: w,
23+
ResponseLength: -1,
24+
StatusCode: -1,
25+
}
26+
27+
start := time.Now()
28+
next.ServeHTTP(crw, r)
29+
requestDuration := time.Since(start)
30+
31+
if r.Method == http.MethodOptions && !logOptions {
32+
return
33+
}
34+
35+
var requestLog *zerolog.Event
36+
if crw.StatusCode >= 500 {
37+
requestLog = log.Error()
38+
} else if crw.StatusCode >= 400 {
39+
requestLog = log.Warn()
40+
} else {
41+
requestLog = log.Info()
42+
}
43+
44+
if userAgent := r.UserAgent(); userAgent != "" {
45+
requestLog.Str("user_agent", userAgent)
46+
}
47+
if referer := r.Referer(); referer != "" {
48+
requestLog.Str("referer", referer)
49+
}
50+
remoteAddr := r.RemoteAddr
51+
52+
requestLog.Str("remote_addr", remoteAddr)
53+
requestLog.Str("method", r.Method)
54+
requestLog.Str("proto", r.Proto)
55+
requestLog.Int64("request_length", r.ContentLength)
56+
requestLog.Str("host", r.Host)
57+
requestLog.Str("request_uri", r.RequestURI)
58+
if r.Method != http.MethodGet && r.Method != http.MethodHead {
59+
requestLog.Str("request_content_type", r.Header.Get("Content-Type"))
60+
if crw.RequestBody != nil {
61+
logRequestMaybeJSON(requestLog, "request_body", crw.RequestBody.Bytes())
62+
}
63+
}
64+
65+
// response
66+
requestLog.Int64("request_time_ms", requestDuration.Milliseconds())
67+
requestLog.Int("status_code", crw.StatusCode)
68+
requestLog.Int("response_length", crw.ResponseLength)
69+
requestLog.Str("response_content_type", crw.Header().Get("Content-Type"))
70+
if crw.ResponseBody != nil {
71+
logRequestMaybeJSON(requestLog, "response_body", crw.ResponseBody.Bytes())
72+
}
73+
74+
// don't log successful health requests
75+
if r.URL.Path == "/health" && crw.StatusCode == http.StatusNoContent {
76+
return
77+
}
78+
79+
requestLog.Msg("Access")
80+
})
81+
}
82+
}
83+
84+
func logRequestMaybeJSON(evt *zerolog.Event, key string, data []byte) {
85+
data = removeNewlines(data)
86+
if json.Valid(data) {
87+
evt.RawJSON(key, data)
88+
} else {
89+
// Logging as a string will create lots of escaping and it's not valid json anyway, so cut off a bit more
90+
if len(data) > MaxStringRequestSizeLog {
91+
data = data[:MaxStringRequestSizeLog]
92+
}
93+
evt.Bytes(key+"_invalid", data)
94+
}
95+
}
96+
97+
func removeNewlines(data []byte) []byte {
98+
data = bytes.TrimSpace(data)
99+
if bytes.ContainsRune(data, '\n') {
100+
data = bytes.ReplaceAll(data, []byte{'\n'}, []byte{})
101+
data = bytes.ReplaceAll(data, []byte{'\r'}, []byte{})
102+
}
103+
return data
104+
}

requestlog/countingresponsewriter.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package requestlog
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"strings"
10+
)
11+
12+
type CountingResponseWriter struct {
13+
StatusCode int
14+
ResponseLength int
15+
Hijacked bool
16+
ResponseWriter http.ResponseWriter
17+
ResponseBody *bytes.Buffer
18+
RequestBody *bytes.Buffer
19+
}
20+
21+
func (crw *CountingResponseWriter) Header() http.Header {
22+
return crw.ResponseWriter.Header()
23+
}
24+
25+
func (crw *CountingResponseWriter) Write(data []byte) (int, error) {
26+
if crw.ResponseLength == -1 {
27+
crw.ResponseLength = 0
28+
}
29+
if crw.StatusCode == -1 {
30+
crw.StatusCode = http.StatusOK
31+
}
32+
crw.ResponseLength += len(data)
33+
34+
if crw.ResponseBody != nil && crw.ResponseBody.Len() < MaxRequestSizeLog {
35+
crw.ResponseBody.Write(CutRequestData(data, crw.ResponseBody.Len()))
36+
}
37+
return crw.ResponseWriter.Write(data)
38+
}
39+
40+
func (crw *CountingResponseWriter) WriteHeader(statusCode int) {
41+
crw.StatusCode = statusCode
42+
crw.ResponseWriter.WriteHeader(statusCode)
43+
if !strings.HasPrefix(crw.Header().Get("Content-Type"), "application/json") {
44+
crw.ResponseBody = nil
45+
}
46+
}
47+
48+
func (crw *CountingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
49+
hijacker, ok := crw.ResponseWriter.(http.Hijacker)
50+
if !ok {
51+
return nil, nil, fmt.Errorf("CountingResponseWriter: %T does not implement http.Hijacker", crw.ResponseWriter)
52+
}
53+
crw.Hijacked = true
54+
return hijacker.Hijack()
55+
}
56+
57+
func CutRequestData(data []byte, length int) []byte {
58+
if len(data)+length > MaxRequestSizeLog {
59+
return data[:MaxRequestSizeLog-length]
60+
}
61+
return data
62+
}

requestlog/route.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package requestlog
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"strings"
8+
)
9+
10+
type Route struct {
11+
Path string
12+
Method string
13+
Handler http.HandlerFunc
14+
15+
TrackHTTPMetrics func(*Route) func(*CountingResponseWriter)
16+
17+
LogContent bool
18+
}
19+
20+
var _ http.Handler = (*Route)(nil)
21+
22+
func (rt *Route) ServeHTTP(w http.ResponseWriter, r *http.Request) {
23+
crw := w.(*CountingResponseWriter)
24+
if rt.TrackHTTPMetrics != nil {
25+
defer rt.TrackHTTPMetrics(rt)(crw)
26+
}
27+
if rt.LogContent {
28+
if r.Method != http.MethodGet && r.Method != http.MethodHead {
29+
crw.ResponseBody = &bytes.Buffer{}
30+
}
31+
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
32+
pcr := &partialCachingReader{Reader: r.Body}
33+
crw.RequestBody = &pcr.Buffer
34+
r.Body = pcr
35+
}
36+
}
37+
rt.Handler(w, r)
38+
}
39+
40+
type partialCachingReader struct {
41+
Reader io.ReadCloser
42+
Buffer bytes.Buffer
43+
}
44+
45+
func (pcr *partialCachingReader) Read(p []byte) (int, error) {
46+
n, err := pcr.Reader.Read(p)
47+
if n > 0 {
48+
pcr.Buffer.Write(CutRequestData(p[:n], pcr.Buffer.Len()))
49+
}
50+
return n, err
51+
}
52+
53+
func (pcr *partialCachingReader) Close() error {
54+
return pcr.Reader.Close()
55+
}

0 commit comments

Comments
 (0)