package server import ( "bufio" "fmt" "io" "net" "net/http" "sync" "time" ) // AccessLog wraps h so every response writes one line to w: // // // // For the attach websocket the line appears when the connection ends, with // status 101 and the connection's lifetime as latency. func AccessLog(h http.Handler, w io.Writer) http.Handler { var mu sync.Mutex return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { sw := &statusWriter{ResponseWriter: rw} start := time.Now() h.ServeHTTP(sw, r) status := sw.status if status == 0 { status = http.StatusOK } line := fmt.Sprintf("%s %s %s %d %s\n", start.Format("2006-01-02T15:04:05.000Z07:00"), r.Method, r.URL.RequestURI(), status, time.Since(start).Round(time.Microsecond)) mu.Lock() _, _ = io.WriteString(w, line) mu.Unlock() }) } // statusWriter records the status code. It passes hijacking through so the // attach endpoint's websocket upgrade keeps working. type statusWriter struct { http.ResponseWriter status int } func (s *statusWriter) WriteHeader(code int) { if s.status == 0 { s.status = code } s.ResponseWriter.WriteHeader(code) } func (s *statusWriter) Write(b []byte) (int, error) { if s.status == 0 { s.status = http.StatusOK } return s.ResponseWriter.Write(b) } // Unwrap lets http.ResponseController reach the underlying writer. func (s *statusWriter) Unwrap() http.ResponseWriter { return s.ResponseWriter } func (s *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { h, ok := s.ResponseWriter.(http.Hijacker) if !ok { return nil, nil, fmt.Errorf("underlying ResponseWriter does not support hijacking") } if s.status == 0 { s.status = http.StatusSwitchingProtocols } return h.Hijack() } func (s *statusWriter) Flush() { if f, ok := s.ResponseWriter.(http.Flusher); ok { f.Flush() } }