package ripsawlogger import ( "encoding/base64" "encoding/json" "fmt" "net/http" "strings" "time" ) var ( MagicHeaderPrefix = "X-Log-" DefaultTokenReader = func(key string, r *http.Request) interface{} { return nil } tokens = []string{ "issuer", "transactionId", "requestId", "parentRequestId", "userId", "brandId", "details", } ) type TokenReader func(key string, r *http.Request) interface{} // Because we use X-Log- headers to add stuff to the access logs, we might wind up // forwarding them elsewhere too if we try to proxy the request. This dumb piece of // work strips them out, calls the handler, and then adds them back in to avoid badness. type GhettoLogConcealer struct { http.Handler } func (ghsp GhettoLogConcealer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // collect all the log headers logs := make(map[string][]string) for k, v := range r.Header { if strings.HasPrefix(k, MagicHeaderPrefix) { logs[k] = v delete(r.Header, k) } } // call the normal handler ghsp.Handler.ServeHTTP(w, r) // add the log headers back in for k, v := range logs { r.Header[k] = v } } type accessLogger struct { Delegate http.Handler TokenReader TokenReader Logger Logger } type monitoredResponseWriter struct { http.ResponseWriter Status int Length int } func (rw *monitoredResponseWriter) Write(data []byte) (int, error) { if rw.Status == 0 { rw.Status = 200 } rw.Length += len(data) return rw.ResponseWriter.Write(data) } func (rw *monitoredResponseWriter) WriteHeader(status int) { rw.Status = status rw.ResponseWriter.WriteHeader(status) } func (al accessLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { // start a timer start := time.Now() // determine the host and via headers via := make([]string, 0, 1) if r.Header.Get("X-Forwarded-For") != "" { via = append(via, strings.Split(r.Header.Get("X-Forwarded-For"), ",")...) } via = append(via, r.RemoteAddr) // trim whitespace from each IP for idx, v := range via { via[idx] = strings.TrimSpace(v) } accessLog := map[string]interface{}{ "clientIP": via[0], "method": r.Method, "url": r.URL.String(), "httpVersion": fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor), } // add a variety of custom tokens, if any value is provided for _, key := range tokens { if value := al.TokenReader(key, r); value != nil { accessLog[key] = value } } // perform the request watcher := monitoredResponseWriter{w, 0, 0} al.Delegate.ServeHTTP(&watcher, r) // add post-response metadata accessLog["time"] = float64(time.Now().Sub(start)) / float64(time.Millisecond) accessLog["status"] = watcher.Status accessLog["bytes"] = watcher.Length // add the User Agent if there is one if len(r.Header["User-Agent"]) > 0 { accessLog["userAgent"] = strings.Join(r.Header["User-Agent"], ", ") } // add the Referer if there is one if len(r.Header["Referer"]) > 0 { accessLog["referer"] = strings.Join(r.Header["Referer"], ", ") } // add the proxy hops if there are any if len(via[1:]) > 0 { accessLog["via"] = via[1:] } // read magic request headers and add them to the access logs for key, values := range r.Header { if strings.HasPrefix(key, MagicHeaderPrefix) { key := denormalizeHeaderName(key[len(MagicHeaderPrefix):]) accessLog[key] = strings.Join(values, ", ") } } // send the log line to the writer al.Logger.Log(LogEntry{Level: LOG_ACCESS, Contents: accessLog}) } func AccessLogger(h http.Handler, tokens TokenReader) http.Handler { // default token implementation if tokens == nil { tokens = DefaultTokenReader } return accessLogger{h, tokens, getDefaultLogger()} } func denormalizeHeaderName(key string) string { normalizedKey := strings.Split(key, "-") normalizedKey[0] = strings.ToLower(normalizedKey[0]) return strings.Join(normalizedKey, "") } func GetTokenFromJWT(key string, r *http.Request) interface{} { jwtTokens := map[string]string{ "issuer": "iss", "userId": "userId", "brandId": "brandId", } for tokenName, claimName := range jwtTokens { if tokenName == key { if tokenParts := strings.Split(r.Header.Get("X-JWT"), "."); len(tokenParts) == 3 { if claimStr, err := base64.RawURLEncoding.DecodeString(tokenParts[1]); err == nil { // parse out the JWT claims claims := make(map[string]interface{}) if err := json.Unmarshal([]byte(claimStr), &claims); err != nil { return nil } // check for our key in the claims if value, ok := claims[claimName]; ok { return value } } } } } return nil }