package middleware import ( "net" "net/http" "time" "sync" myhttp "amuz.es/src/infra/goutils/http" "amuz.es/src/infra/goutils/buf" "amuz.es/src/infra/goutils/misc" "io" ) const ( dateFormat = "02/Jan/2006:15:04:05 -0700" forwardedForIPHeader = "X-Forwarded-For" ) type bufferedWriter interface { WriteTo(w io.Writer) (n int64, err error) Write(p []byte) (n int, err error) } type accessLoggerMiddleware struct { serverNameValue []string writer io.Writer waiter *sync.WaitGroup dumpQ bufferedWriter writerFlushChan chan bool } func (m *accessLoggerMiddleware) Handle(next http.Handler) http.Handler { accessLoggerBufPool := buf.ByteBufferPool{} fn := func(w http.ResponseWriter, r *http.Request) { m.waiter.Add(1) respWrapper := myhttp.NewWrapResponseWriter(w, r.ProtoMajor) defer func(now time.Time) { dur := time.Since(now) buf := accessLoggerBufPool.Acquire() defer accessLoggerBufPool.Release(buf) buf.WriteString(m.remoteHost(r)) buf.WriteString(` - - [`) buf.WriteString(now.Format(dateFormat)) buf.WriteString(`] "`) buf.WriteString(r.Method) buf.WriteByte(' ') buf.WriteString(r.RequestURI) buf.WriteByte(' ') buf.WriteString(r.Proto) buf.WriteString(`" `) buf.WriteString(misc.FormatInt(respWrapper.Status())) buf.WriteByte(' ') buf.WriteString(misc.FormatInt(respWrapper.BytesWritten())) buf.WriteString(` "`) buf.WriteString(r.Referer()) buf.WriteString(`" "`) buf.WriteString(r.UserAgent()) buf.WriteString(`" `) buf.WriteString(misc.FormatInt64(dur.Nanoseconds() / time.Millisecond.Nanoseconds())) buf.WriteByte(' ') buf.WriteString(r.Host) buf.WriteByte('\n') m.dumpQ.Write(buf.B) m.waiter.Done() }(time.Now().Local()) respWrapper.Header()["Server"] = m.serverNameValue next.ServeHTTP(respWrapper, r) } return http.HandlerFunc(fn) } func (m *accessLoggerMiddleware) Close() { m.waiter.Wait() m.writerFlushChan <- true <-m.writerFlushChan } func (m *accessLoggerMiddleware) lineByLineWriter() { ticker := time.NewTicker(250 * time.Millisecond) defer func() { ticker.Stop() m.dumpQ.WriteTo(m.writer) close(m.writerFlushChan) }() for { <-ticker.C m.dumpQ.WriteTo(m.writer) select { case <-m.writerFlushChan: return default: } } } // strip port from addresses with hostname, ipv4 or ipv6 func (m *accessLoggerMiddleware) stripPort(address string) string { if h, _, err := net.SplitHostPort(address); err == nil { return h } return address } // The remote address of the client. When the 'X-Forwarded-For' // header is set, then it is used instead. func (m *accessLoggerMiddleware) remoteAddr(r *http.Request) string { ff := r.Header.Get(forwardedForIPHeader) if ff != "" { return ff } return r.RemoteAddr } func (m *accessLoggerMiddleware) remoteHost(r *http.Request) string { a := m.remoteAddr(r) h := m.stripPort(a) if h != "" { return h } return "-" } func AccessLog(serverName string, bufferingWriter bufferedWriter, accessLogWriter io.Writer) (func(next http.Handler) http.Handler, func()) { impl := &accessLoggerMiddleware{} impl.serverNameValue = append(impl.serverNameValue, serverName) impl.writer = accessLogWriter impl.waiter = &sync.WaitGroup{} impl.writerFlushChan = make(chan bool, 1) impl.dumpQ = bufferingWriter go impl.lineByLineWriter() return impl.Handle, impl.Close }