├── .travis.yml ├── LICENSE ├── README.md ├── context.go ├── context_test.go ├── example ├── main.go └── screenshot.png ├── go.mod ├── go.sum ├── header.go ├── header_test.go ├── metric.go ├── metric_test.go ├── middleware.go └── middleware_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - tip 6 | 7 | script: 8 | - go test 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Server-Timing for Go 2 | [![Godoc](https://godoc.org/github.com/mitchellh/go-server-timing?status.svg)](https://godoc.org/github.com/mitchellh/go-server-timing) 3 | 4 | This is a library including middleware for using 5 | [HTTP Server-Timing](https://www.w3.org/TR/server-timing) with Go. This header 6 | allows a server to send timing information from the backend, such as database 7 | access time, file reads, etc. The timing information can be then be inspected 8 | in the standard browser developer tools: 9 | 10 | ![Server Timing Example](https://raw.githubusercontent.com/mitchellh/go-server-timing/master/example/screenshot.png) 11 | 12 | ## Features 13 | 14 | * Middleware for injecting the server timing struct into the request `Context` 15 | and writing the `Server-Timing` header. 16 | 17 | * Concurrency-safe structures for easily recording timings of multiple 18 | concurrency tasks. 19 | 20 | * Parse `Server-Timing` headers as a client. 21 | 22 | * Note: No browser properly supports sending the Server-Timing header as 23 | an [HTTP Trailer](https://tools.ietf.org/html/rfc7230#section-4.4) so 24 | the Middleware only supports a normal header currently. 25 | 26 | ## Browser Support 27 | 28 | Browser support is required to **view** server timings easily. Because server 29 | timings are sent as an HTTP header, there is no negative impact to sending 30 | the header to unsupported browsers. 31 | 32 | * Either **Chrome 65 or higher** or **Firefox 71 or higher** is required 33 | to properly display server timings in the devtools. 34 | 35 | * IE, Opera, and others are unknown at this time. 36 | 37 | ## Usage 38 | 39 | Example usage is shown below. A fully runnable example is available in 40 | the `example/` directory. 41 | 42 | ```go 43 | func main() { 44 | // Our handler. In a real application this might be your root router, 45 | // or some subset of your router. Wrapping this ensures that all routes 46 | // handled by this handler have access to the server timing header struct. 47 | var h http.Handler = http.HandlerFunc(handler) 48 | 49 | // Wrap our handler with the server timing middleware 50 | h = servertiming.Middleware(h, nil) 51 | 52 | // Start! 53 | http.ListenAndServe(":8080", h) 54 | } 55 | 56 | func handler(w http.ResponseWriter, r *http.Request) { 57 | // Get our timing header builder from the context 58 | timing := servertiming.FromContext(r.Context()) 59 | 60 | // Imagine your handler performs some tasks in a goroutine, such as 61 | // accessing some remote service. timing is concurrency safe so we can 62 | // record how long that takes. Let's simulate making 5 concurrent requests 63 | // to various servicse. 64 | var wg sync.WaitGroup 65 | for i := 0; i < 5; i++ { 66 | wg.Add(1) 67 | name := fmt.Sprintf("service-%d", i) 68 | go func(name string) { 69 | // This creats a new metric and starts the timer. The Stop is 70 | // deferred so when the function exits it'll record the duration. 71 | defer timing.NewMetric(name).Start().Stop() 72 | time.Sleep(random(25, 75)) 73 | wg.Done() 74 | }(name) 75 | } 76 | 77 | // Imagine this is just some blocking code in your main handler such 78 | // as a SQL query. Let's record that. 79 | m := timing.NewMetric("sql").WithDesc("SQL query").Start() 80 | time.Sleep(random(20, 50)) 81 | m.Stop() 82 | 83 | // Wait for the goroutine to end 84 | wg.Wait() 85 | 86 | // You could continue recording more metrics, but let's just return now 87 | w.WriteHeader(200) 88 | w.Write([]byte("Done. Check your browser inspector timing details.")) 89 | } 90 | 91 | func random(min, max int) time.Duration { 92 | return (time.Duration(rand.Intn(max-min) + min)) * time.Millisecond 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // NewContext returns a new Context that carries the Header value h. 8 | func NewContext(ctx context.Context, h *Header) context.Context { 9 | return context.WithValue(ctx, contextKey, h) 10 | } 11 | 12 | // FromContext returns the *Header in the context, if any. If no Header 13 | // value exists, nil is returned. 14 | func FromContext(ctx context.Context) *Header { 15 | h, _ := ctx.Value(contextKey).(*Header) 16 | return h 17 | } 18 | 19 | type contextKeyType struct{} 20 | 21 | // The key where the header value is stored. This is globally unique since 22 | // it uses a custom unexported type. The struct{} costs zero allocations. 23 | var contextKey = contextKeyType(struct{}{}) 24 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestContext(t *testing.T) { 9 | h := new(Header) 10 | ctx := NewContext(context.Background(), h) 11 | h2 := FromContext(ctx) 12 | if h != h2 { 13 | t.Fatal("should have stored value") 14 | } 15 | } 16 | 17 | func TestContext_notSet(t *testing.T) { 18 | h := FromContext(context.Background()) 19 | if h != nil { 20 | t.Fatal("h should be nil") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/mitchellh/go-server-timing" 12 | ) 13 | 14 | func init() { 15 | rand.Seed(time.Now().Unix()) 16 | } 17 | 18 | func main() { 19 | // Our handler. In a real application this might be your root router, 20 | // or some subset of your router. Wrapping this ensures that all routes 21 | // handled by this handler have access to the server timing header struct. 22 | var h http.Handler = http.HandlerFunc(handler) 23 | 24 | // Wrap our handler with the server timing middleware 25 | h = servertiming.Middleware(h, nil) 26 | 27 | // Let the user know what to do for this example 28 | println("Visit http://127.0.0.1:8080") 29 | 30 | // Start! 31 | log.Fatal(http.ListenAndServe(":8080", h)) 32 | } 33 | 34 | func handler(w http.ResponseWriter, r *http.Request) { 35 | // Get our timing header builder from the context 36 | timing := servertiming.FromContext(r.Context()) 37 | 38 | // Imagine your handler performs some tasks in a goroutine, such as 39 | // accessing some remote service. timing is concurrency safe so we can 40 | // record how long that takes. Let's simulate making 5 concurrent requests 41 | // to various servicse. 42 | var wg sync.WaitGroup 43 | for i := 0; i < 5; i++ { 44 | wg.Add(1) 45 | name := fmt.Sprintf("service-%d", i) 46 | go func(name string) { 47 | // This creats a new metric and starts the timer. The Stop is 48 | // deferred so when the function exits it'll record the duration. 49 | defer timing.NewMetric(name).Start().Stop() 50 | time.Sleep(random(25, 75)) 51 | wg.Done() 52 | }(name) 53 | } 54 | 55 | // Imagine this is just some blocking code in your main handler such 56 | // as a SQL query. Let's record that. 57 | m := timing.NewMetric("sql").WithDesc("SQL query").Start() 58 | time.Sleep(random(20, 50)) 59 | m.Stop() 60 | 61 | // Wait for the goroutine to end 62 | wg.Wait() 63 | 64 | // You could continue recording more metrics, but let's just return now 65 | w.WriteHeader(200) 66 | _, err := w.Write([]byte("Done. Check your browser inspector timing details.")) 67 | if err != nil { 68 | log.Printf("Can't write http response: %s", err) 69 | } 70 | } 71 | 72 | func random(min, max int) time.Duration { 73 | return (time.Duration(rand.Intn(max-min) + min)) * time.Millisecond 74 | } 75 | -------------------------------------------------------------------------------- /example/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchellh/go-server-timing/feb680ab92c20d57c527399b842e1941bde888c3/example/screenshot.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitchellh/go-server-timing 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/felixge/httpsnoop v1.0.0 7 | github.com/go-delve/delve v1.5.0 // indirect 8 | github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5 9 | github.com/google/go-cmp v0.4.1 // indirect 10 | github.com/google/go-dap v0.3.0 // indirect 11 | github.com/mattn/go-colorable v0.1.8 // indirect 12 | github.com/mattn/go-runewidth v0.0.9 // indirect 13 | github.com/peterh/liner v1.2.0 // indirect 14 | github.com/sirupsen/logrus v1.7.0 // indirect 15 | github.com/spf13/cobra v1.0.0 // indirect 16 | github.com/spf13/pflag v1.0.5 // indirect 17 | go.starlark.net v0.0.0-20201006213952-227f4aabceb5 // indirect 18 | golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff // indirect 19 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 // indirect 20 | gopkg.in/yaml.v2 v2.3.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 11 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 12 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 14 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 15 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 16 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 17 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 18 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 19 | github.com/cosiner/argv v0.1.0 h1:BVDiEL32lwHukgJKP87btEPenzrrHUjajs/8yzaqcXg= 20 | github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= 21 | github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= 22 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 24 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 25 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 28 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 29 | github.com/felixge/httpsnoop v1.0.0 h1:gh8fMGz0rlOv/1WmRZm7OgncIOTsAj21iNJot48omJQ= 30 | github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8= 31 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 32 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 33 | github.com/go-delve/delve v1.5.0 h1:gQsRvFdR0BGk19NROQZsAv6iG4w5QIZoJlxJeEUBb0c= 34 | github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ= 35 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 36 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 37 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 38 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 39 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 40 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 41 | github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5 h1:yrv1uUvgXH/tEat+wdvJMRJ4g51GlIydtDpU9pFjaaI= 42 | github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= 43 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 44 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 45 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 46 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 48 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 49 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 50 | github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= 51 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/go-dap v0.2.0 h1:whjIGQRumwbR40qRU7CEKuFLmePUUc2s4Nt9DoXXxWk= 53 | github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= 54 | github.com/google/go-dap v0.3.0 h1:Dc4izN0u4VhZERYrz80f1PSEoDsVfdVmdM/W82CxzFk= 55 | github.com/google/go-dap v0.3.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= 56 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 57 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 58 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 59 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 60 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 61 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 62 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 63 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 64 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 65 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 66 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 67 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 68 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 69 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 70 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 71 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 72 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 73 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 74 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 75 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 76 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 77 | github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561 h1:isR/L+BIZ+rqODWYR/f526ygrBMGKZYFhaaFRDGvuZ8= 78 | github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 79 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 80 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 81 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= 82 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 83 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 84 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 85 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 86 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 87 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 88 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 89 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 90 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 91 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 92 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 93 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 94 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 95 | github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b h1:8uaXtUkxiy+T/zdLWuxa/PG4so0TPZDZfafFNNSaptE= 96 | github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= 97 | github.com/peterh/liner v1.2.0 h1:w/UPXyl5GfahFxcTOz2j9wCIHNI+pUPr2laqpojKNCg= 98 | github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= 99 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 100 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 102 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 103 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 104 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 105 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 106 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 107 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 108 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 109 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 110 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 111 | github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 112 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 113 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 114 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 115 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 116 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 117 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 118 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 119 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 120 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 121 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 122 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 123 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 124 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 125 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 126 | github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372 h1:eRfW1vRS4th8IX2iQeyqQ8cOUNOySvAYJ0IUvTXGoYA= 127 | github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 128 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 129 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 130 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 131 | github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1 h1:7bozMfSdo41n2NOc0GsVTTVUiA+Ncaj6pXNpm4UHKys= 132 | github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 133 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 134 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 135 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 136 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 137 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 138 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 139 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 140 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 141 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 142 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 143 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 144 | go.starlark.net v0.0.0-20190702223751-32f345186213 h1:lkYv5AKwvvduv5XWP6szk/bvvgO6aDeUujhZQXIFTes= 145 | go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= 146 | go.starlark.net v0.0.0-20201006213952-227f4aabceb5 h1:ApvY/1gw+Yiqb/FKeks3KnVPWpkR3xzij82XPKLjJVw= 147 | go.starlark.net v0.0.0-20201006213952-227f4aabceb5/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= 148 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 149 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 150 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 151 | golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4 h1:QlVATYS7JBoZMVaf+cNjb90WD/beKVHnIxFKT4QaHVI= 152 | golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 153 | golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff h1:XmKBi9R6duxOB3lfc72wyrwiOY7X2Jl1wuI+RFOyMDE= 154 | golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 155 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 156 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 157 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 158 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 159 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 160 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 161 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 163 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 164 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 165 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 166 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 172 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= 177 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM= 183 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 185 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 186 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 187 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 188 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 189 | golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 190 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 192 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 193 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 194 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 195 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 196 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 197 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 198 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 199 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 200 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 201 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 202 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 203 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 204 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 205 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 206 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 207 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 208 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 209 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/golang/gddo/httputil/header" 12 | ) 13 | 14 | // HeaderKey is the specified key for the Server-Timing header. 15 | const HeaderKey = "Server-Timing" 16 | 17 | // Header represents a collection of metrics that can be encoded as 18 | // a Server-Timing header value. 19 | // 20 | // The functions for working with metrics are concurrency-safe to make 21 | // it easy to record metrics from goroutines. If you want to avoid the 22 | // lock overhead, you can access the Metrics field directly. 23 | // 24 | // The functions for working with metrics are also usable on a nil 25 | // Header pointer. This allows functions that use FromContext to get the 26 | // *Header value to skip nil-checking and use it as normal. On a nil 27 | // *Header, Metrics are not recorded. 28 | type Header struct { 29 | // Metrics is the list of metrics in the header. 30 | Metrics []*Metric 31 | 32 | // The lock that is held when Metrics is being modified. This 33 | // ONLY NEEDS TO BE SET WHEN working with Metrics directly. If using 34 | // the functions on the struct, the lock is managed automatically. 35 | sync.Mutex 36 | } 37 | 38 | // ParseHeader parses a Server-Timing header value. 39 | func ParseHeader(input string) (*Header, error) { 40 | // Split the comma-separated list of metrics 41 | rawMetrics := header.ParseList(headerParams(input)) 42 | 43 | // Parse the list of metrics. We can pre-allocate the length of the 44 | // comma-separated list of metrics since at most it will be that and 45 | // most likely it will be that length. 46 | metrics := make([]*Metric, 0, len(rawMetrics)) 47 | for _, raw := range rawMetrics { 48 | var m Metric 49 | m.Name, m.Extra = header.ParseValueAndParams(headerParams(raw)) 50 | 51 | // Description 52 | if v, ok := m.Extra[paramNameDesc]; ok { 53 | m.Desc = v 54 | delete(m.Extra, paramNameDesc) 55 | } 56 | 57 | // Duration. This is treated as a millisecond value since that 58 | // is what modern browsers are treating it as. If the parsing of 59 | // an integer fails, the set value remains in the Extra field. 60 | if v, ok := m.Extra[paramNameDur]; ok { 61 | m.Duration, _ = time.ParseDuration(v + "ms") 62 | delete(m.Extra, paramNameDur) 63 | } 64 | 65 | metrics = append(metrics, &m) 66 | } 67 | 68 | return &Header{Metrics: metrics}, nil 69 | } 70 | 71 | // NewMetric creates a new Metric and adds it to this header. 72 | func (h *Header) NewMetric(name string) *Metric { 73 | return h.Add(&Metric{Name: name}) 74 | } 75 | 76 | // Add adds the given metric to the header. 77 | // 78 | // This function is safe to call concurrently. 79 | func (h *Header) Add(m *Metric) *Metric { 80 | if h == nil { 81 | return m 82 | } 83 | 84 | h.Lock() 85 | defer h.Unlock() 86 | h.Metrics = append(h.Metrics, m) 87 | return m 88 | } 89 | 90 | // String returns the valid Server-Timing header value that can be 91 | // sent in an HTTP response. 92 | func (h *Header) String() string { 93 | parts := make([]string, 0, len(h.Metrics)) 94 | for _, m := range h.Metrics { 95 | parts = append(parts, m.String()) 96 | } 97 | 98 | return strings.Join(parts, ",") 99 | } 100 | 101 | // Specified server-timing-param-name values. 102 | const ( 103 | paramNameDesc = "desc" 104 | paramNameDur = "dur" 105 | ) 106 | 107 | // headerParams is a helper function that takes a header value and turns 108 | // it into the expected argument format for the httputil/header library 109 | // functions.. 110 | func headerParams(s string) (http.Header, string) { 111 | const key = "Key" 112 | return http.Header(map[string][]string{ 113 | key: {s}, 114 | }), key 115 | } 116 | 117 | var reNumber = regexp.MustCompile(`^\d+\.?\d*$`) 118 | 119 | // headerEncodeParam encodes a key/value pair as a proper `key=value` 120 | // syntax, using double-quotes if necessary. 121 | func headerEncodeParam(key, value string) string { 122 | // The only case we currently don't quote is numbers. We can make this 123 | // smarter in the future. 124 | if reNumber.MatchString(value) { 125 | return fmt.Sprintf(`%s=%s`, key, value) 126 | } 127 | 128 | return fmt.Sprintf(`%s=%q`, key, value) 129 | } 130 | -------------------------------------------------------------------------------- /header_test.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // headerCases contains test cases for the Server-Timing header. This set 10 | // of test cases is used to test both parsing the header value as well as 11 | // generating the correct header value. 12 | var headerCases = []struct { 13 | Metrics []*Metric 14 | HeaderValue string 15 | }{ 16 | { 17 | []*Metric{ 18 | { 19 | Name: "sql-1", 20 | Duration: 100 * time.Millisecond, 21 | Desc: "MySQL lookup Server", 22 | Extra: map[string]string{}, 23 | }, 24 | }, 25 | `sql-1;desc="MySQL lookup Server";dur=100`, 26 | }, 27 | 28 | // Comma in description 29 | { 30 | []*Metric{ 31 | { 32 | Name: "sql-1", 33 | Duration: 100 * time.Millisecond, 34 | Desc: "MySQL, lookup Server", 35 | Extra: map[string]string{}, 36 | }, 37 | }, 38 | `sql-1;desc="MySQL, lookup Server";dur=100`, 39 | }, 40 | 41 | // Semicolon in description 42 | { 43 | []*Metric{ 44 | { 45 | Name: "sql-1", 46 | Duration: 100 * time.Millisecond, 47 | Desc: "MySQL; lookup Server", 48 | Extra: map[string]string{}, 49 | }, 50 | }, 51 | `sql-1;desc="MySQL; lookup Server";dur=100`, 52 | }, 53 | 54 | // Description that contains a number 55 | { 56 | []*Metric{ 57 | { 58 | Name: "sql-1", 59 | Duration: 100 * time.Millisecond, 60 | Desc: "GET 200", 61 | Extra: map[string]string{}, 62 | }, 63 | }, 64 | `sql-1;desc="GET 200";dur=100`, 65 | }, 66 | 67 | // Number that contains floating point 68 | { 69 | []*Metric{ 70 | { 71 | Name: "sql-1", 72 | Duration: 100100 * time.Microsecond, 73 | Desc: "MySQL; lookup Server", 74 | Extra: map[string]string{}, 75 | }, 76 | }, 77 | `sql-1;desc="MySQL; lookup Server";dur=100.1`, 78 | }, 79 | } 80 | 81 | func TestParseHeader(t *testing.T) { 82 | for _, tt := range headerCases { 83 | t.Run(tt.HeaderValue, func(t *testing.T) { 84 | h, err := ParseHeader(tt.HeaderValue) 85 | if err != nil { 86 | t.Fatalf("error parsing header: %s", err) 87 | } 88 | 89 | if !reflect.DeepEqual(h.Metrics, tt.Metrics) { 90 | t.Fatalf("received, expected:\n\n%#v\n\n%#v", h.Metrics, tt.Metrics) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | func TestHeaderString(t *testing.T) { 97 | for _, tt := range headerCases { 98 | t.Run(tt.HeaderValue, func(t *testing.T) { 99 | h := &Header{Metrics: tt.Metrics} 100 | actual := h.String() 101 | if actual != tt.HeaderValue { 102 | t.Fatalf("received, expected:\n\n%q\n\n%q", actual, tt.HeaderValue) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | // Same as TestHeaderString but using the Add method 109 | func TestHeaderAdd(t *testing.T) { 110 | for _, tt := range headerCases { 111 | t.Run(tt.HeaderValue, func(t *testing.T) { 112 | var h Header 113 | for _, m := range tt.Metrics { 114 | h.Add(m) 115 | } 116 | 117 | actual := h.String() 118 | if actual != tt.HeaderValue { 119 | t.Fatalf("received, expected:\n\n%q\n\n%q", actual, tt.HeaderValue) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /metric.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Metric represents a single metric for the Server-Timing header. 11 | // 12 | // The easiest way to use the Metric is to use NewMetric and chain it. This 13 | // results in a single line defer at the top of a function time a function. 14 | // 15 | // timing := FromContext(r.Context()) 16 | // defer timing.NewMetric("sql").Start().Stop() 17 | // 18 | // For timing around specific blocks of code: 19 | // 20 | // m := timing.NewMetric("sql").Start() 21 | // // ... run your code being timed here 22 | // m.Stop() 23 | // 24 | // A metric is expected to represent a single timing event. Therefore, 25 | // no functions on the struct are safe for concurrency by default. If a single 26 | // Metric is shared by multiple concurrenty goroutines, you must lock access 27 | // manually. 28 | type Metric struct { 29 | // Name is the name of the metric. This must be a valid RFC7230 "token" 30 | // format. In a gist, this is an alphanumeric string that may contain 31 | // most common symbols but may not contain any whitespace. The exact 32 | // syntax can be found in RFC7230. 33 | // 34 | // It is common for Name to be a unique identifier (such as "sql-1") and 35 | // for a more human-friendly name to be used in the "desc" field. 36 | Name string 37 | 38 | // Duration is the duration of this Metric. 39 | Duration time.Duration 40 | 41 | // Desc is any string describing this metric. For example: "SQL Primary". 42 | // The specific format of this is `token | quoted-string` according to 43 | // RFC7230. 44 | Desc string 45 | 46 | // Extra is a set of extra parameters and values to send with the 47 | // metric. The specification states that unrecognized parameters are 48 | // to be ignored so it should be safe to add additional data here. The 49 | // key must be a valid "token" (same syntax as Name) and the value can 50 | // be any "token | quoted-string" (same as Desc field). 51 | // 52 | // If this map contains a key that would be sent by another field in this 53 | // struct (such as "desc"), then this value is prioritized over the 54 | // struct value. 55 | Extra map[string]string 56 | 57 | // startTime is the time that this metric recording was started if 58 | // Start() was called. 59 | startTime time.Time 60 | } 61 | 62 | // WithDesc is a chaining-friendly helper to set the Desc field on the Metric. 63 | func (m *Metric) WithDesc(desc string) *Metric { 64 | m.Desc = desc 65 | return m 66 | } 67 | 68 | // Start starts a timer for recording the duration of some task. This must 69 | // be paired with a Stop call to set the duration. Calling this again will 70 | // reset the start time for a subsequent Stop call. 71 | func (m *Metric) Start() *Metric { 72 | m.startTime = time.Now() 73 | return m 74 | } 75 | 76 | // Stop ends the timer started with Start and records the duration in the 77 | // Duration field. Calling this multiple times will modify the Duration based 78 | // on the last time Start was called. 79 | // 80 | // If Start was never called, this function has zero effect. 81 | func (m *Metric) Stop() *Metric { 82 | // Only record if we have a start time set with Start() 83 | if !m.startTime.IsZero() { 84 | m.Duration = time.Since(m.startTime) 85 | } 86 | 87 | return m 88 | } 89 | 90 | // String returns the valid Server-Timing metric entry value. 91 | func (m *Metric) String() string { 92 | // Begin building parts, expected capacity is length of extra 93 | // fields plus id, desc, dur. 94 | parts := make([]string, 1, len(m.Extra)+3) 95 | parts[0] = m.Name 96 | 97 | // Description 98 | if _, ok := m.Extra[paramNameDesc]; !ok && m.Desc != "" { 99 | parts = append(parts, headerEncodeParam(paramNameDesc, m.Desc)) 100 | } 101 | 102 | // Duration 103 | if _, ok := m.Extra[paramNameDur]; !ok && m.Duration > 0 { 104 | parts = append(parts, headerEncodeParam( 105 | paramNameDur, 106 | strconv.FormatFloat(float64(m.Duration)/float64(time.Millisecond), 'f', -1, 64), 107 | )) 108 | } 109 | 110 | // All remaining extra params 111 | for k, v := range m.Extra { 112 | parts = append(parts, headerEncodeParam(k, v)) 113 | } 114 | 115 | return strings.Join(parts, ";") 116 | } 117 | 118 | // GoString is needed for fmt.GoStringer so %v works on pointer value. 119 | func (m *Metric) GoString() string { 120 | if m == nil { 121 | return "nil" 122 | } 123 | 124 | return fmt.Sprintf("*%#v", *m) 125 | } 126 | -------------------------------------------------------------------------------- /metric_test.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestMetric_startStop(t *testing.T) { 9 | var m Metric 10 | m.Start() 11 | time.Sleep(50 * time.Millisecond) 12 | m.Stop() 13 | 14 | actual := m.Duration 15 | if actual == 0 { 16 | t.Fatal("duration should be set") 17 | } 18 | if actual > 100*time.Millisecond { 19 | t.Fatal("expected duration to be within 100ms") 20 | } 21 | if actual < 30*time.Millisecond { 22 | t.Fatal("expected duration to be more than 30ms") 23 | } 24 | } 25 | 26 | func TestMetric_stopNoStart(t *testing.T) { 27 | var m Metric 28 | m.Stop() 29 | 30 | actual := m.Duration 31 | if actual != 0 { 32 | t.Fatal("duration should not be set") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/felixge/httpsnoop" 7 | ) 8 | 9 | // MiddlewareOpts are options for the Middleware. 10 | type MiddlewareOpts struct { 11 | // Don’t write headers in the request. Metrics are still gathered though. 12 | DisableHeaders bool 13 | // Maybe more in the future. 14 | } 15 | 16 | // Middleware wraps an http.Handler and provides a *Header in the request 17 | // context that can be used to set Server-Timing headers. The *Header can be 18 | // extracted from the context using FromContext. 19 | // 20 | // The options supplied to this can be nil to use defaults. 21 | // 22 | // The Server-Timing header will be written when the status is written 23 | // only if there are non-empty number of metrics. 24 | // 25 | // To control when Server-Timing is sent, the easiest approach is to wrap 26 | // this middleware and only call it if the request should send server timings. 27 | // For examples, see the README. 28 | func Middleware(next http.Handler, opts *MiddlewareOpts) http.Handler { 29 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 30 | var ( 31 | // Create the Server-Timing headers struct 32 | h Header 33 | // Remember if the timing header were added to the response headers 34 | headerWritten bool 35 | ) 36 | 37 | // This places the *Header value into the request context. This 38 | // can be extracted again with FromContext. 39 | r = r.WithContext(NewContext(r.Context(), &h)) 40 | 41 | // Get the header map. This is a reference and shouldn't change. 42 | headers := w.Header() 43 | 44 | // Hook the response writer we pass upstream so we can modify headers 45 | // before they write them to the wire, but after we know what status 46 | // they are writing. 47 | hooks := httpsnoop.Hooks{ 48 | WriteHeader: func(original httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { 49 | // Return a function with same signature as 50 | // http.ResponseWriter.WriteHeader to be called in it's place 51 | return func(code int) { 52 | // Write the headers and remember that headers were written 53 | writeHeader(headers, &h, opts) 54 | headerWritten = true 55 | 56 | // Call the original WriteHeader function 57 | original(code) 58 | } 59 | }, 60 | 61 | Write: func(original httpsnoop.WriteFunc) httpsnoop.WriteFunc { 62 | return func(b []byte) (int, error) { 63 | // If we didn't write headers, then we have to do that 64 | // first before any data is written. 65 | if !headerWritten { 66 | writeHeader(headers, &h, opts) 67 | headerWritten = true 68 | } 69 | 70 | return original(b) 71 | } 72 | }, 73 | } 74 | 75 | w = httpsnoop.Wrap(w, hooks) 76 | next.ServeHTTP(w, r) 77 | 78 | // In case that next did not called WriteHeader function, add timing header to the response headers 79 | if !headerWritten { 80 | writeHeader(headers, &h, opts) 81 | } 82 | }) 83 | } 84 | 85 | func writeHeader(headers http.Header, h *Header, opts *MiddlewareOpts) { 86 | // Grab the lock just in case there is any ongoing concurrency that 87 | // still has a reference and may be modifying the value. 88 | h.Lock() 89 | defer h.Unlock() 90 | 91 | // If there are no metrics set, or if the user opted-out writing headers, 92 | // do nothing 93 | if (opts != nil && opts.DisableHeaders) || len(h.Metrics) == 0 { 94 | return 95 | } 96 | 97 | headers.Set(HeaderKey, h.String()) 98 | } 99 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package servertiming 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | responseBody = "response" 12 | responseStatus = http.StatusCreated 13 | ) 14 | 15 | func TestMiddleware(t *testing.T) { 16 | cases := []struct { 17 | Name string 18 | Opts *MiddlewareOpts 19 | Metrics []*Metric 20 | SkipWriteHeaders bool 21 | Expected bool 22 | }{ 23 | { 24 | Name: "nil metrics", 25 | Metrics: nil, 26 | Expected: false, 27 | }, 28 | 29 | { 30 | Name: "empty metrics", 31 | Metrics: []*Metric{}, 32 | Expected: false, 33 | }, 34 | 35 | { 36 | Name: "single metric disable headers option", 37 | Opts: &MiddlewareOpts{DisableHeaders: true}, 38 | Metrics: []*Metric{ 39 | { 40 | Name: "sql-1", 41 | Duration: 100 * time.Millisecond, 42 | Desc: "MySQL; lookup Server", 43 | }, 44 | }, 45 | Expected: false, 46 | }, 47 | 48 | { 49 | Name: "single metric", 50 | Metrics: []*Metric{ 51 | { 52 | Name: "sql-1", 53 | Duration: 100 * time.Millisecond, 54 | Desc: "MySQL; lookup Server", 55 | }, 56 | }, 57 | Expected: true, 58 | }, 59 | 60 | { 61 | Name: "single metric without invoking WriteHeaders in handler", 62 | Metrics: []*Metric{ 63 | { 64 | Name: "sql-1", 65 | Duration: 100 * time.Millisecond, 66 | Desc: "MySQL; lookup Server", 67 | }, 68 | }, 69 | Expected: true, 70 | SkipWriteHeaders: true, 71 | }, 72 | } 73 | 74 | for _, tt := range cases { 75 | t.Run(tt.Name, func(t *testing.T) { 76 | r := httptest.NewRequest("GET", "/", nil) 77 | rec := httptest.NewRecorder() 78 | 79 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 80 | // Set the metrics to the configured case 81 | h := FromContext(r.Context()) 82 | if h == nil { 83 | t.Fatal("expected *Header to be present in context") 84 | } 85 | h.Metrics = tt.Metrics 86 | 87 | // Write the header to flush the response 88 | if !tt.SkipWriteHeaders { 89 | w.WriteHeader(responseStatus) 90 | } 91 | 92 | // Write date to response body 93 | w.Write([]byte(responseBody)) 94 | }) 95 | 96 | // Perform the request 97 | Middleware(handler, tt.Opts).ServeHTTP(rec, r) 98 | 99 | // Test that it is present or not 100 | _, present := map[string][]string(rec.Header())[HeaderKey] 101 | if present != tt.Expected { 102 | t.Fatalf("expected header to be present: %v, but wasn't", tt.Expected) 103 | } 104 | 105 | // Test the response headers 106 | expected := (&Header{Metrics: tt.Metrics}).String() 107 | if tt.Opts != nil && tt.Opts.DisableHeaders == true { 108 | expected = "" 109 | } 110 | actual := rec.Header().Get(HeaderKey) 111 | if actual != expected { 112 | t.Fatalf("got wrong value, expected != actual: %q != %q", expected, actual) 113 | } 114 | 115 | // Test the status code of the response, if we skip the write headers method, the default 200 should be 116 | // the response status code 117 | expectedStatus := responseStatus 118 | if tt.SkipWriteHeaders { 119 | expectedStatus = http.StatusOK 120 | } 121 | if actualStatus := rec.Result().StatusCode; expectedStatus != actualStatus { 122 | t.Fatalf("got unexpected status code value, expected != actual: %q != %q", expectedStatus, actualStatus) 123 | } 124 | 125 | // Test the response body was left intact 126 | if responseBody != rec.Body.String() { 127 | t.Fatalf("got unexpected body, expected != actual: %q != %q", responseBody, rec.Body.String()) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | // We need to test this separately since the httptest.ResponseRecorder 134 | // doesn't properly reflect that headers can't be set after writing data, 135 | // so we have to use a real server. 136 | func TestMiddleware_writeHeaderNotCalled(t *testing.T) { 137 | metrics := []*Metric{ 138 | { 139 | Name: "sql-1", 140 | Duration: 100 * time.Millisecond, 141 | Desc: "MySQL; lookup Server", 142 | }, 143 | } 144 | 145 | // Start our test server 146 | ts := httptest.NewServer(Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 147 | // Set the metrics to the configured case 148 | h := FromContext(r.Context()) 149 | if h == nil { 150 | t.Fatal("expected *Header to be present in context") 151 | } 152 | 153 | h.Metrics = metrics 154 | 155 | // Write date to response body WITHOUT calling WriteHeader 156 | w.Write([]byte(responseBody)) 157 | }), nil)) 158 | defer ts.Close() 159 | 160 | res, err := http.Get(ts.URL) 161 | if err != nil { 162 | t.Fatal(err) 163 | } 164 | 165 | // Test that it is present or not 166 | _, present := map[string][]string(res.Header)[HeaderKey] 167 | if !present { 168 | t.Fatal("expected header to be present") 169 | } 170 | 171 | // Test the response headers 172 | expected := (&Header{Metrics: metrics}).String() 173 | actual := res.Header.Get(HeaderKey) 174 | if actual != expected { 175 | t.Fatalf("got wrong value, expected != actual: %q != %q", expected, actual) 176 | } 177 | } 178 | --------------------------------------------------------------------------------