blob: 79c64601329690924c6895b7bead9b6793db2ee3 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package relui
import (
"context"
"fmt"
"mime"
"net/http"
"path"
"strings"
"time"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/julienschmidt/httprouter"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
"golang.org/x/build/internal/relui/db"
)
var (
kDBQueryName = tag.MustNewKey("go-build/relui/keys/db/query-name")
mDBLatency = stats.Float64("go-build/relui/db/latency", "Database query latency by query name", stats.UnitMilliseconds)
)
// Views should contain all measurements. All *view.View added to this
// slice will be registered and exported to the metric service.
var Views = []*view.View{
{
Name: "go-build/relui/http/server/latency",
Description: "Latency distribution of HTTP requests",
Measure: ochttp.ServerLatency,
TagKeys: []tag.Key{ochttp.KeyServerRoute},
Aggregation: ochttp.DefaultLatencyDistribution,
},
{
Name: "go-build/relui/http/server/response_count_by_status_code",
Description: "Server response count by status code",
TagKeys: []tag.Key{ochttp.StatusCode, ochttp.KeyServerRoute},
Measure: ochttp.ServerLatency,
Aggregation: view.Count(),
},
{
Name: "go-build/relui/db/query_latency",
Description: "Latency distribution of database queries",
TagKeys: []tag.Key{kDBQueryName},
Measure: mDBLatency,
Aggregation: ochttp.DefaultLatencyDistribution,
},
}
// metricsRouter wraps an *httprouter.Router with telemetry.
type metricsRouter struct {
router *httprouter.Router
}
// GET is shorthand for Handle(http.MethodGet, path, handle)
func (r *metricsRouter) GET(path string, handle httprouter.Handle) {
r.Handle(http.MethodGet, path, handle)
}
// HEAD is shorthand for Handle(http.MethodHead, path, handle)
func (r *metricsRouter) HEAD(path string, handle httprouter.Handle) {
r.Handle(http.MethodHead, path, handle)
}
// OPTIONS is shorthand for Handle(http.MethodOptions, path, handle)
func (r *metricsRouter) OPTIONS(path string, handle httprouter.Handle) {
r.Handle(http.MethodOptions, path, handle)
}
// POST is shorthand for Handle(http.MethodPost, path, handle)
func (r *metricsRouter) POST(path string, handle httprouter.Handle) {
r.Handle(http.MethodPost, path, handle)
}
// PUT is shorthand for Handle(http.MethodPut, path, handle)
func (r *metricsRouter) PUT(path string, handle httprouter.Handle) {
r.Handle(http.MethodPut, path, handle)
}
// PATCH is shorthand for Handle(http.MethodPatch, path, handle)
func (r *metricsRouter) PATCH(path string, handle httprouter.Handle) {
r.Handle(http.MethodPatch, path, handle)
}
// DELETE is shorthand for Handle(http.MethodDelete, path, handle)
func (r *metricsRouter) DELETE(path string, handle httprouter.Handle) {
r.Handle(http.MethodDelete, path, handle)
}
// Handler wraps *httprouter.Handler with recorded metrics.
func (r *metricsRouter) Handler(method, path string, handler http.Handler) {
r.router.Handler(method, path, ochttp.WithRouteTag(handler, path))
}
// HandlerFunc wraps *httprouter.HandlerFunc with recorded metrics.
func (r *metricsRouter) HandlerFunc(method, path string, handler http.HandlerFunc) {
r.Handler(method, path, handler)
}
// ServeFiles serves files at the specified root. The provided path
// must end in /*filepath.
//
// Unlike *httprouter.ServeFiles, this method sets a Content-Type and
// Cache-Control to "no-cache, private, max-age=0". This handler
// also does not strip the prefix of the request path.
func (r *metricsRouter) ServeFiles(p string, root http.FileSystem) {
if len(p) < 10 || p[len(p)-10:] != "/*filepath" {
panic(fmt.Sprintf("p must end with /*filepath in path %q", p))
}
s := http.FileServer(root)
r.GET(p, func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(req.URL.Path)))
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
s.ServeHTTP(w, req)
})
}
// Lookup wraps *httprouter.Lookup.
func (r *metricsRouter) Lookup(method, path string) (httprouter.Handle, httprouter.Params, bool) {
return r.router.Lookup(method, path)
}
// ServeHTTP wraps *httprouter.ServeHTTP.
func (r *metricsRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.router.ServeHTTP(w, req)
}
// Handle calls *httprouter.ServeHTTP with additional metrics reporting.
func (r *metricsRouter) Handle(method, path string, handle httprouter.Handle) {
r.router.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
ochttp.WithRouteTag(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handle(w, r, params)
}), path).ServeHTTP(w, r)
})
}
type MetricsDB struct {
db.PGDBTX
}
func (m *MetricsDB) Exec(ctx context.Context, s string, i ...any) (pgconn.CommandTag, error) {
defer recordDB(ctx, time.Now(), queryName(s))
return m.PGDBTX.Exec(ctx, s, i...)
}
func (m *MetricsDB) Query(ctx context.Context, s string, i ...any) (pgx.Rows, error) {
defer recordDB(ctx, time.Now(), queryName(s))
return m.PGDBTX.Query(ctx, s, i...)
}
func (m *MetricsDB) QueryRow(ctx context.Context, s string, i ...any) pgx.Row {
defer recordDB(ctx, time.Now(), queryName(s))
return m.PGDBTX.QueryRow(ctx, s, i...)
}
func recordDB(ctx context.Context, start time.Time, name string) {
stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kDBQueryName, name)},
mDBLatency.M(float64(time.Since(start))/float64(time.Millisecond)))
}
func queryName(s string) string {
prefix := "-- name: "
if !strings.HasPrefix(s, prefix) {
return "Unknown"
}
rest := s[len(prefix):]
return rest[:strings.IndexRune(rest, ' ')]
}