1
0
Fork 0

API: expose runtime representation

Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
Co-authored-by: Jean-Baptiste Doumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
mpl 2019-05-16 10:58:06 +02:00 committed by Traefiker Bot
parent 5cd9396dae
commit f6df556eb0
50 changed files with 2250 additions and 1158 deletions

View file

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Florent Messa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,59 +0,0 @@
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
}

View file

@ -1,95 +0,0 @@
package stats
import (
"bufio"
"fmt"
"net"
"net/http"
)
type ResponseWriter interface {
http.ResponseWriter
http.Flusher
// Status returns the status code of the response or 0 if the response has not been written.
Status() int
// Written returns whether or not the ResponseWriter has been written.
Written() bool
// Size returns the size of the response body.
Size() int
// Before allows for a function to be called before the ResponseWriter has been written to. This is
// useful for setting headers or any other operations that must happen before a response has been written.
Before(func(ResponseWriter))
}
type beforeFunc func(ResponseWriter)
type recorderResponseWriter struct {
http.ResponseWriter
status int
size int
beforeFuncs []beforeFunc
written bool
}
func NewRecorderResponseWriter(w http.ResponseWriter, statusCode int) ResponseWriter {
return &recorderResponseWriter{ResponseWriter: w, status: statusCode}
}
func (r *recorderResponseWriter) WriteHeader(code int) {
r.written = true
r.ResponseWriter.WriteHeader(code)
r.status = code
}
func (r *recorderResponseWriter) Flush() {
flusher, ok := r.ResponseWriter.(http.Flusher)
if ok {
flusher.Flush()
}
}
func (r *recorderResponseWriter) Status() int {
return r.status
}
func (r *recorderResponseWriter) Write(b []byte) (int, error) {
if !r.Written() {
// The status will be StatusOK if WriteHeader has not been called yet
r.WriteHeader(http.StatusOK)
}
size, err := r.ResponseWriter.Write(b)
r.size += size
return size, err
}
// Proxy method to Status to add support for gocraft
func (r *recorderResponseWriter) StatusCode() int {
return r.Status()
}
func (r *recorderResponseWriter) Size() int {
return r.size
}
func (r *recorderResponseWriter) Written() bool {
return r.StatusCode() != 0
}
func (r *recorderResponseWriter) CloseNotify() <-chan bool {
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
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")
}
return hijacker.Hijack()
}
func (r *recorderResponseWriter) Before(before func(ResponseWriter)) {
r.beforeFuncs = append(r.beforeFuncs, before)
}

View file

@ -1,227 +0,0 @@
package stats
import (
"fmt"
"net/http"
"os"
"sync"
"time"
)
// 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 {
select {
case <-stats.closed:
return
default:
stats.ResetResponseCounts()
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()
defer mw.mu.Unlock()
mw.ResponseCounts = map[string]int{}
}
// Handler is a MiddlewareFunc makes Stats implement the Middleware interface.
func (mw *Stats) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
beginning, recorder := mw.Begin(w)
h.ServeHTTP(recorder, r)
mw.End(beginning, WithRecorder(recorder))
})
}
// Negroni compatible interface
func (mw *Stats) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
beginning, recorder := mw.Begin(w)
next(recorder, r)
mw.End(beginning, WithRecorder(recorder))
}
// Begin starts a recorder
func (mw *Stats) Begin(w http.ResponseWriter) (time.Time, ResponseWriter) {
start := time.Now()
writer := NewRecorderResponseWriter(w, 200)
return start, writer
}
// End closes the recorder with a specific status
func (mw *Stats) End(start time.Time, opts ...Option) {
options := newOptions(opts...)
responseTime := time.Since(start)
mw.mu.Lock()
defer mw.mu.Unlock()
// 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())
}
}
// 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"`
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()
uptime := now.Sub(mw.Uptime)
count := 0
for code, current := range mw.ResponseCounts {
responseCounts[code] = current
count += current
}
totalCount := 0
for code, count := range mw.TotalResponseCounts {
totalResponseCounts[code] = count
totalCount += count
}
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()
r := &Data{
Pid: mw.Pid,
UpTime: uptime.String(),
UpTimeSec: uptime.Seconds(),
Time: now.String(),
TimeUnix: now.Unix(),
StatusCodeCount: responseCounts,
TotalStatusCodeCount: totalResponseCounts,
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
}