feat: add response time middleware

This commit is contained in:
franklin 2021-09-07 10:10:21 +02:00
parent af26e561fb
commit 9830f28b99
6 changed files with 128 additions and 8 deletions

View File

@ -438,6 +438,36 @@ func ExampleSessionID() {
svr.Run() svr.Run()
} }
func ExmampleResponseTime() {
svr := keel.NewServer()
// get logger
l := svr.Logger()
// create demo service
svs := http.NewServeMux()
svs.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
svs.HandleFunc("/sleep", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
svr.AddService(
keel.NewServiceHTTP(l, "demo", ":8080", svs,
middleware.ResponseTime(
// automatically set cookie if not exists
middleware.ResponseTimeWithMaxDuration(time.Millisecond * 500),
),
),
)
svr.Run()
}
func ExampleSkip() { func ExampleSkip() {
svr := keel.NewServer() svr := keel.NewServer()

View File

@ -24,6 +24,7 @@ const (
HeaderXUrlScheme = "X-Url-Scheme" HeaderXUrlScheme = "X-Url-Scheme"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
HeaderXRealIP = "X-Real-IP" HeaderXRealIP = "X-Real-IP"
HeaderXResponseTime = "X-Response-Time"
HeaderXRequestID = "X-Request-ID" HeaderXRequestID = "X-Request-ID"
HeaderXRequestedWith = "X-Requested-With" HeaderXRequestedWith = "X-Requested-With"
HeaderXSessionID = "X-Session-ID" HeaderXSessionID = "X-Session-ID"

View File

@ -37,7 +37,7 @@ func LoggerWithOptions(opts LoggerOptions) Middleware {
start := time.Now() start := time.Now()
// wrap response write to get access to status & size // wrap response write to get access to status & size
wr := wrapResponseWriter(w) wr := WrapResponseWriter(w)
next.ServeHTTP(wr, r) next.ServeHTTP(wr, r)

View File

@ -0,0 +1,75 @@
package middleware
import (
"net/http"
"time"
"go.uber.org/zap"
"github.com/foomo/keel/log"
)
type (
ResponseTimeOptions struct {
SetHeader bool
MaxDuration time.Duration
MaxDurationMessage string
}
ResponseTimeOption func(*ResponseTimeOptions)
)
// GetDefaultResponseTimeOptions returns the default options
func GetDefaultResponseTimeOptions() ResponseTimeOptions {
return ResponseTimeOptions{
SetHeader: true,
MaxDurationMessage: "max response time exceeded",
}
}
// ResponseTimeWithMaxDurationMessage middleware option
func ResponseTimeWithMaxDurationMessage(v string) ResponseTimeOption {
return func(o *ResponseTimeOptions) {
o.MaxDurationMessage = v
}
}
// ResponseTimeWithMaxDuration middleware option
func ResponseTimeWithMaxDuration(v time.Duration) ResponseTimeOption {
return func(o *ResponseTimeOptions) {
o.MaxDuration = v
}
}
// ResponseTimeWithSetHeader middleware option
func ResponseTimeWithSetHeader(v bool) ResponseTimeOption {
return func(o *ResponseTimeOptions) {
o.SetHeader = v
}
}
// ResponseTime middleware
func ResponseTime(opts ...ResponseTimeOption) Middleware {
options := GetDefaultResponseTimeOptions()
for _, opt := range opts {
if opt != nil {
opt(&options)
}
}
return ResponseTimeWithOptions(options)
}
// ResponseTimeWithOptions middleware
func ResponseTimeWithOptions(opts ResponseTimeOptions) Middleware {
return func(l *zap.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := WrapResponseWriter(w)
rw.SetWriteResponseTimeHeader(opts.SetHeader)
next.ServeHTTP(rw, r)
duration := time.Since(start)
if opts.MaxDuration > 0 && duration > opts.MaxDuration {
l.Warn(opts.MaxDurationMessage, log.FDuration(opts.MaxDuration), log.FValue(duration.Microseconds()))
}
})
}
}

View File

@ -3,25 +3,36 @@ package middleware
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"time"
http2 "github.com/foomo/keel/net/http"
) )
// responseWriter is a wrapper that includes that http statusCode and size for logging // responseWriter is a wrapper that includes that http statusCode and size for logging
type responseWriter struct { type responseWriter struct {
http.ResponseWriter http.ResponseWriter
wroteHeader bool writeResponseTimeHeader bool
statusCode int wroteHeader bool
size int statusCode int
start time.Time
size int
} }
func wrapResponseWriter(w http.ResponseWriter) *responseWriter { func WrapResponseWriter(w http.ResponseWriter) *responseWriter {
if wr, ok := w.(*responseWriter); ok { if wr, ok := w.(*responseWriter); ok {
return wr return wr
} }
return &responseWriter{ return &responseWriter{
ResponseWriter: w, ResponseWriter: w,
start: time.Now(),
} }
} }
func (w *responseWriter) SetWriteResponseTimeHeader(write bool) {
w.writeResponseTimeHeader = write
}
func (w *responseWriter) Size() int { func (w *responseWriter) Size() int {
return w.size return w.size
} }
@ -38,9 +49,12 @@ func (w *responseWriter) Status() string {
} }
func (w *responseWriter) WriteHeader(statusCode int) { func (w *responseWriter) WriteHeader(statusCode int) {
w.wroteHeader = true if w.writeResponseTimeHeader {
w.statusCode = statusCode w.Header().Set(http2.HeaderXResponseTime, strconv.FormatInt(time.Since(w.start).Microseconds(), 10))
}
w.ResponseWriter.WriteHeader(statusCode) w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
w.wroteHeader = true
} }
func (w *responseWriter) Write(b []byte) (int, error) { func (w *responseWriter) Write(b []byte) (int, error) {

View File

@ -36,7 +36,7 @@ func TelemetryWithOptions(name string, opts TelemetryOptions) Middleware {
return func(l *zap.Logger, next http.Handler) http.Handler { return func(l *zap.Logger, next http.Handler) http.Handler {
return otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// wrap response write to get access to status & size // wrap response write to get access to status & size
wr := wrapResponseWriter(w) wr := WrapResponseWriter(w)
start := time.Now() start := time.Now()
next.ServeHTTP(wr, r) next.ServeHTTP(wr, r)