diff --git a/Gopkg.lock b/Gopkg.lock index c5017a407..9d2c7930e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1469,11 +1469,11 @@ revision = "c4434f09ec131ecf30f986d5dcb1636508bfa49a" [[projects]] - digest = "1:c269070c7f286c941392076d6c42a4a7a062356f7083a48bd202397e877a965e" + digest = "1:84b9a5318d8ce3b8a9b1509bf15734f4f9dcd4decf9d9e9c7346a16c7b64d49e" name = "github.com/thoas/stats" packages = ["."] pruneopts = "NUT" - revision = "152b5d051953fdb6e45f14b6826962aadc032324" + revision = "4975baf6a358ed3ddaa42133996e1959f96c9300" [[projects]] branch = "master" diff --git a/Gopkg.toml b/Gopkg.toml index 066224842..7251f0fd4 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -85,9 +85,10 @@ required = [ name = "github.com/containous/staert" version = "3.1.2" -#[[constraint]] -# name = "github.com/containous/traefik-extra-service-fabric" -# version = "1.3.0" +[[constraint]] + name = "github.com/thoas/stats" + # related to https://github.com/thoas/stats/pull/32 + revision = "4975baf6a358ed3ddaa42133996e1959f96c9300" [[constraint]] name = "github.com/coreos/go-systemd" diff --git a/vendor/github.com/thoas/stats/options.go b/vendor/github.com/thoas/stats/options.go new file mode 100644 index 000000000..5db124f24 --- /dev/null +++ b/vendor/github.com/thoas/stats/options.go @@ -0,0 +1,59 @@ +package stats + +// Options are stats options. +type Options struct { + statusCode *int + size int + recorder ResponseWriter +} + +// StatusCode returns the response status code. +func (o Options) StatusCode() int { + if o.recorder != nil { + return o.recorder.Status() + } + + return *o.statusCode +} + +// Size returns the response size. +func (o Options) Size() int { + if o.recorder != nil { + return o.recorder.Size() + } + + return o.size +} + +// Option represents a stats option. +type Option func(*Options) + +// WithStatusCode sets the status code to use in stats. +func WithStatusCode(statusCode int) Option { + return func(o *Options) { + o.statusCode = &statusCode + } +} + +// WithSize sets the size to use in stats. +func WithSize(size int) Option { + return func(o *Options) { + o.size = size + } +} + +// WithRecorder sets the recorder to use in stats. +func WithRecorder(recorder ResponseWriter) Option { + return func(o *Options) { + o.recorder = recorder + } +} + +// newOptions takes functional options and returns options. +func newOptions(options ...Option) *Options { + opts := &Options{} + for _, o := range options { + o(opts) + } + return opts +} diff --git a/vendor/github.com/thoas/stats/recorder.go b/vendor/github.com/thoas/stats/recorder.go index 4b489643c..326042c24 100644 --- a/vendor/github.com/thoas/stats/recorder.go +++ b/vendor/github.com/thoas/stats/recorder.go @@ -28,13 +28,15 @@ type recorderResponseWriter struct { status int size int beforeFuncs []beforeFunc + written bool } func NewRecorderResponseWriter(w http.ResponseWriter, statusCode int) ResponseWriter { - return &recorderResponseWriter{w, statusCode, 0, nil} + return &recorderResponseWriter{ResponseWriter: w, status: statusCode} } func (r *recorderResponseWriter) WriteHeader(code int) { + r.written = true r.ResponseWriter.WriteHeader(code) r.status = code } @@ -78,6 +80,9 @@ func (r *recorderResponseWriter) CloseNotify() <-chan bool { } func (r *recorderResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if !r.written { + r.status = 0 + } hijacker, ok := r.ResponseWriter.(http.Hijacker) if !ok { return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") diff --git a/vendor/github.com/thoas/stats/stats.go b/vendor/github.com/thoas/stats/stats.go index edd9247f4..65dc2f99c 100644 --- a/vendor/github.com/thoas/stats/stats.go +++ b/vendor/github.com/thoas/stats/stats.go @@ -11,34 +11,58 @@ import ( // Stats data structure type Stats struct { mu sync.RWMutex + closed chan struct{} + Hostname string Uptime time.Time Pid int ResponseCounts map[string]int TotalResponseCounts map[string]int TotalResponseTime time.Time + TotalResponseSize int64 + MetricsCounts map[string]int + MetricsTimers map[string]time.Time +} + +// Label data structure +type Label struct { + Name string + Value string } // New constructs a new Stats structure func New() *Stats { + name, _ := os.Hostname() + stats := &Stats{ + closed: make(chan struct{}, 1), Uptime: time.Now(), Pid: os.Getpid(), ResponseCounts: map[string]int{}, TotalResponseCounts: map[string]int{}, TotalResponseTime: time.Time{}, + Hostname: name, } go func() { for { - stats.ResetResponseCounts() + select { + case <-stats.closed: + return + default: + stats.ResetResponseCounts() - time.Sleep(time.Second * 1) + time.Sleep(time.Second * 1) + } } }() return stats } +func (mw *Stats) Close() { + close(mw.closed) +} + // ResetResponseCounts reset the response counts func (mw *Stats) ResetResponseCounts() { mw.mu.Lock() @@ -53,7 +77,7 @@ func (mw *Stats) Handler(h http.Handler) http.Handler { h.ServeHTTP(recorder, r) - mw.End(beginning, recorder) + mw.End(beginning, WithRecorder(recorder)) }) } @@ -63,7 +87,7 @@ func (mw *Stats) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.Han next(recorder, r) - mw.End(beginning, recorder) + mw.End(beginning, WithRecorder(recorder)) } // Begin starts a recorder @@ -75,52 +99,73 @@ func (mw *Stats) Begin(w http.ResponseWriter) (time.Time, ResponseWriter) { return start, writer } -// EndWithStatus closes the recorder with a specific status -func (mw *Stats) EndWithStatus(start time.Time, status int) { - end := time.Now() +// End closes the recorder with a specific status +func (mw *Stats) End(start time.Time, opts ...Option) { + options := newOptions(opts...) - responseTime := end.Sub(start) + responseTime := time.Since(start) mw.mu.Lock() defer mw.mu.Unlock() - statusCode := fmt.Sprintf("%d", status) - - mw.ResponseCounts[statusCode]++ - mw.TotalResponseCounts[statusCode]++ - mw.TotalResponseTime = mw.TotalResponseTime.Add(responseTime) + // If Hijacked connection do not count in response time + if options.StatusCode() != 0 { + statusCode := fmt.Sprintf("%d", options.StatusCode()) + mw.ResponseCounts[statusCode]++ + mw.TotalResponseCounts[statusCode]++ + mw.TotalResponseTime = mw.TotalResponseTime.Add(responseTime) + mw.TotalResponseSize += int64(options.Size()) + } } -// End closes the recorder with the recorder status -func (mw *Stats) End(start time.Time, recorder ResponseWriter) { - mw.EndWithStatus(start, recorder.Status()) +// MeasureSince method for execution time recording +func (mw *Stats) MeasureSince(key string, start time.Time) { + mw.MeasureSinceWithLabels(key, start, nil) +} + +// MeasureSinceWithLabels method for execution time recording with custom labels +func (mw *Stats) MeasureSinceWithLabels(key string, start time.Time, labels []Label) { + labels = append(labels, Label{"host", mw.Hostname}) + elapsed := time.Since(start) + + mw.mu.Lock() + defer mw.mu.Unlock() + + mw.MetricsCounts[key]++ + mw.MetricsTimers[key] = mw.MetricsTimers[key].Add(elapsed) } // Data serializable structure type Data struct { - Pid int `json:"pid"` - UpTime string `json:"uptime"` - UpTimeSec float64 `json:"uptime_sec"` - Time string `json:"time"` - TimeUnix int64 `json:"unixtime"` - StatusCodeCount map[string]int `json:"status_code_count"` - TotalStatusCodeCount map[string]int `json:"total_status_code_count"` - Count int `json:"count"` - TotalCount int `json:"total_count"` - TotalResponseTime string `json:"total_response_time"` - TotalResponseTimeSec float64 `json:"total_response_time_sec"` - AverageResponseTime string `json:"average_response_time"` - AverageResponseTimeSec float64 `json:"average_response_time_sec"` + Pid int `json:"pid"` + Hostname string `json:"hostname"` + UpTime string `json:"uptime"` + UpTimeSec float64 `json:"uptime_sec"` + Time string `json:"time"` + TimeUnix int64 `json:"unixtime"` + StatusCodeCount map[string]int `json:"status_code_count"` + TotalStatusCodeCount map[string]int `json:"total_status_code_count"` + Count int `json:"count"` + TotalCount int `json:"total_count"` + TotalResponseTime string `json:"total_response_time"` + TotalResponseTimeSec float64 `json:"total_response_time_sec"` + TotalResponseSize int64 `json:"total_response_size"` + AverageResponseSize int64 `json:"average_response_size"` + AverageResponseTime string `json:"average_response_time"` + AverageResponseTimeSec float64 `json:"average_response_time_sec"` + TotalMetricsCounts map[string]int `json:"total_metrics_counts"` + AverageMetricsTimers map[string]float64 `json:"average_metrics_timers"` } // Data returns the data serializable structure func (mw *Stats) Data() *Data { - mw.mu.RLock() responseCounts := make(map[string]int, len(mw.ResponseCounts)) totalResponseCounts := make(map[string]int, len(mw.TotalResponseCounts)) + totalMetricsCounts := make(map[string]int, len(mw.MetricsCounts)) + metricsCounts := make(map[string]float64, len(mw.MetricsCounts)) now := time.Now() @@ -139,11 +184,21 @@ func (mw *Stats) Data() *Data { } totalResponseTime := mw.TotalResponseTime.Sub(time.Time{}) + totalResponseSize := mw.TotalResponseSize averageResponseTime := time.Duration(0) + averageResponseSize := int64(0) if totalCount > 0 { avgNs := int64(totalResponseTime) / int64(totalCount) averageResponseTime = time.Duration(avgNs) + averageResponseSize = int64(totalResponseSize) / int64(totalCount) + } + + for key, count := range mw.MetricsCounts { + totalMetric := mw.MetricsTimers[key].Sub(time.Time{}) + avgNs := int64(totalMetric) / int64(count) + metricsCounts[key] = time.Duration(avgNs).Seconds() + totalMetricsCounts[key] = count } mw.mu.RUnlock() @@ -159,9 +214,13 @@ func (mw *Stats) Data() *Data { Count: count, TotalCount: totalCount, TotalResponseTime: totalResponseTime.String(), + TotalResponseSize: totalResponseSize, TotalResponseTimeSec: totalResponseTime.Seconds(), + TotalMetricsCounts: totalMetricsCounts, + AverageResponseSize: averageResponseSize, AverageResponseTime: averageResponseTime.String(), AverageResponseTimeSec: averageResponseTime.Seconds(), + AverageMetricsTimers: metricsCounts, } return r