/
logging_handler.go
131 lines (111 loc) · 3.56 KB
/
logging_handler.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
// largely adapted from https://github.com/gorilla/handlers/blob/master/handlers.go
// to add logging of request duration as last value (and drop referrer)
package proxy
import (
"io"
"net/http"
"net/url"
"strings"
"time"
log "github.com/buzzfeed/sso/internal/pkg/logging"
"github.com/datadog/datadog-go/statsd"
)
// Used to stash the authenticated user in the response for access when logging requests.
const loggingUserHeader = "SSO-Authenticated-User"
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status
// code and body size
type responseLogger struct {
w http.ResponseWriter
status int
size int
authInfo string
}
func (l *responseLogger) Header() http.Header {
return l.w.Header()
}
func (l *responseLogger) extractUser() {
authInfo := l.w.Header().Get(loggingUserHeader)
if authInfo != "" {
l.authInfo = authInfo
l.w.Header().Del(loggingUserHeader)
}
}
func (l *responseLogger) Write(b []byte) (int, error) {
if l.status == 0 {
// The status will be StatusOK if WriteHeader has not been called yet
l.status = http.StatusOK
}
l.extractUser()
size, err := l.w.Write(b)
l.size += size
return size, err
}
func (l *responseLogger) WriteHeader(s int) {
l.extractUser()
l.w.WriteHeader(s)
l.status = s
}
func (l *responseLogger) Status() int {
return l.status
}
func (l *responseLogger) Size() int {
return l.size
}
func (l *responseLogger) Flush() {
f := l.w.(http.Flusher)
f.Flush()
}
// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends
type loggingHandler struct {
writer io.Writer
handler http.Handler
StatsdClient *statsd.Client
enabled bool
}
// NewLoggingHandler returns a new loggingHandler that wraps a handler, statsd client, and writer.
func NewLoggingHandler(out io.Writer, h http.Handler, v bool, StatsdClient *statsd.Client) http.Handler {
return loggingHandler{writer: out,
handler: h,
enabled: v,
StatsdClient: StatsdClient,
}
}
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
now := time.Now()
url := *req.URL
logger := &responseLogger{w: w}
h.handler.ServeHTTP(logger, req)
if !h.enabled {
return
}
logRequest(logger.authInfo, req, url, now, logger.Status(), h.StatsdClient)
}
// logRequest logs information about a request
func logRequest(username string, req *http.Request, url url.URL, ts time.Time, status int, StatsdClient *statsd.Client) {
duration := time.Now().Sub(ts)
// Convert duration to floating point milliseconds
// https://github.com/golang/go/issues/5491#issuecomment-66079585
durationMS := duration.Seconds() * 1e3
uri := req.Host + url.RequestURI()
logger := log.NewLogEntry()
logger.WithHTTPStatus(status).WithRequestMethod(req.Method).WithRequestURI(
uri).WithUserAgent(req.Header.Get("User-Agent")).WithRemoteAddress(
getRemoteAddr(req)).WithRequestDurationMs(durationMS).WithUser(
username).WithAction(GetActionTag(req)).Info()
logRequestMetrics(req, duration, status, StatsdClient)
}
// getRemoteAddr returns the client IP address from a request. If present, the
// X-Forwarded-For header is assumed to be set by a load balancer, and its
// rightmost entry (the client IP that connected to the LB) is returned.
func getRemoteAddr(req *http.Request) string {
addr := req.RemoteAddr
forwardedHeader := req.Header.Get("X-Forwarded-For")
if forwardedHeader != "" {
forwardedList := strings.Split(forwardedHeader, ",")
forwardedAddr := strings.TrimSpace(forwardedList[len(forwardedList)-1])
if forwardedAddr != "" {
addr = forwardedAddr
}
}
return addr
}