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  }