golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/metrics.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package relui 6 7 import ( 8 "context" 9 "fmt" 10 "mime" 11 "net/http" 12 "path" 13 "strings" 14 "time" 15 16 "github.com/jackc/pgconn" 17 "github.com/jackc/pgx/v4" 18 "github.com/julienschmidt/httprouter" 19 "go.opencensus.io/plugin/ochttp" 20 "go.opencensus.io/stats" 21 "go.opencensus.io/stats/view" 22 "go.opencensus.io/tag" 23 "golang.org/x/build/internal/relui/db" 24 ) 25 26 var ( 27 kDBQueryName = tag.MustNewKey("go-build/relui/keys/db/query-name") 28 mDBLatency = stats.Float64("go-build/relui/db/latency", "Database query latency by query name", stats.UnitMilliseconds) 29 ) 30 31 // Views should contain all measurements. All *view.View added to this 32 // slice will be registered and exported to the metric service. 33 var Views = []*view.View{ 34 { 35 Name: "go-build/relui/http/server/latency", 36 Description: "Latency distribution of HTTP requests", 37 Measure: ochttp.ServerLatency, 38 TagKeys: []tag.Key{ochttp.KeyServerRoute}, 39 Aggregation: ochttp.DefaultLatencyDistribution, 40 }, 41 { 42 Name: "go-build/relui/http/server/response_count_by_status_code", 43 Description: "Server response count by status code", 44 TagKeys: []tag.Key{ochttp.StatusCode, ochttp.KeyServerRoute}, 45 Measure: ochttp.ServerLatency, 46 Aggregation: view.Count(), 47 }, 48 { 49 Name: "go-build/relui/db/query_latency", 50 Description: "Latency distribution of database queries", 51 TagKeys: []tag.Key{kDBQueryName}, 52 Measure: mDBLatency, 53 Aggregation: ochttp.DefaultLatencyDistribution, 54 }, 55 } 56 57 // metricsRouter wraps an *httprouter.Router with telemetry. 58 type metricsRouter struct { 59 router *httprouter.Router 60 } 61 62 // GET is shorthand for Handle(http.MethodGet, path, handle) 63 func (r *metricsRouter) GET(path string, handle httprouter.Handle) { 64 r.Handle(http.MethodGet, path, handle) 65 } 66 67 // HEAD is shorthand for Handle(http.MethodHead, path, handle) 68 func (r *metricsRouter) HEAD(path string, handle httprouter.Handle) { 69 r.Handle(http.MethodHead, path, handle) 70 } 71 72 // OPTIONS is shorthand for Handle(http.MethodOptions, path, handle) 73 func (r *metricsRouter) OPTIONS(path string, handle httprouter.Handle) { 74 r.Handle(http.MethodOptions, path, handle) 75 } 76 77 // POST is shorthand for Handle(http.MethodPost, path, handle) 78 func (r *metricsRouter) POST(path string, handle httprouter.Handle) { 79 r.Handle(http.MethodPost, path, handle) 80 } 81 82 // PUT is shorthand for Handle(http.MethodPut, path, handle) 83 func (r *metricsRouter) PUT(path string, handle httprouter.Handle) { 84 r.Handle(http.MethodPut, path, handle) 85 } 86 87 // PATCH is shorthand for Handle(http.MethodPatch, path, handle) 88 func (r *metricsRouter) PATCH(path string, handle httprouter.Handle) { 89 r.Handle(http.MethodPatch, path, handle) 90 } 91 92 // DELETE is shorthand for Handle(http.MethodDelete, path, handle) 93 func (r *metricsRouter) DELETE(path string, handle httprouter.Handle) { 94 r.Handle(http.MethodDelete, path, handle) 95 } 96 97 // Handler wraps *httprouter.Handler with recorded metrics. 98 func (r *metricsRouter) Handler(method, path string, handler http.Handler) { 99 r.router.Handler(method, path, ochttp.WithRouteTag(handler, path)) 100 } 101 102 // HandlerFunc wraps *httprouter.HandlerFunc with recorded metrics. 103 func (r *metricsRouter) HandlerFunc(method, path string, handler http.HandlerFunc) { 104 r.Handler(method, path, handler) 105 } 106 107 // ServeFiles serves files at the specified root. The provided path 108 // must end in /*filepath. 109 // 110 // Unlike *httprouter.ServeFiles, this method sets a Content-Type and 111 // Cache-Control to "no-cache, private, max-age=0". This handler 112 // also does not strip the prefix of the request path. 113 func (r *metricsRouter) ServeFiles(p string, root http.FileSystem) { 114 if len(p) < 10 || p[len(p)-10:] != "/*filepath" { 115 panic(fmt.Sprintf("p must end with /*filepath in path %q", p)) 116 } 117 118 s := http.FileServer(root) 119 r.GET(p, func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { 120 w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(req.URL.Path))) 121 w.Header().Set("Cache-Control", "no-cache, private, max-age=0") 122 s.ServeHTTP(w, req) 123 }) 124 } 125 126 // Lookup wraps *httprouter.Lookup. 127 func (r *metricsRouter) Lookup(method, path string) (httprouter.Handle, httprouter.Params, bool) { 128 return r.router.Lookup(method, path) 129 } 130 131 // ServeHTTP wraps *httprouter.ServeHTTP. 132 func (r *metricsRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { 133 r.router.ServeHTTP(w, req) 134 } 135 136 // Handle calls *httprouter.ServeHTTP with additional metrics reporting. 137 func (r *metricsRouter) Handle(method, path string, handle httprouter.Handle) { 138 r.router.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params httprouter.Params) { 139 ochttp.WithRouteTag(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 140 handle(w, r, params) 141 }), path).ServeHTTP(w, r) 142 }) 143 } 144 145 type MetricsDB struct { 146 db.PGDBTX 147 } 148 149 func (m *MetricsDB) Exec(ctx context.Context, s string, i ...any) (pgconn.CommandTag, error) { 150 defer recordDB(ctx, time.Now(), queryName(s)) 151 return m.PGDBTX.Exec(ctx, s, i...) 152 } 153 154 func (m *MetricsDB) Query(ctx context.Context, s string, i ...any) (pgx.Rows, error) { 155 defer recordDB(ctx, time.Now(), queryName(s)) 156 return m.PGDBTX.Query(ctx, s, i...) 157 } 158 159 func (m *MetricsDB) QueryRow(ctx context.Context, s string, i ...any) pgx.Row { 160 defer recordDB(ctx, time.Now(), queryName(s)) 161 return m.PGDBTX.QueryRow(ctx, s, i...) 162 } 163 164 func recordDB(ctx context.Context, start time.Time, name string) { 165 stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kDBQueryName, name)}, 166 mDBLatency.M(float64(time.Since(start))/float64(time.Millisecond))) 167 } 168 169 func queryName(s string) string { 170 prefix := "-- name: " 171 if !strings.HasPrefix(s, prefix) { 172 return "Unknown" 173 } 174 rest := s[len(prefix):] 175 return rest[:strings.IndexRune(rest, ' ')] 176 }