blob: b7c9f06ed598f00a5991619087c70653b73038f6 [file]
// 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"
"io/fs"
"net/http"
"strings"
"time"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"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 *http.ServeMux with telemetry.
type metricsRouter struct {
mux *http.ServeMux
}
// Handle is like (*http.ServeMux).Handle but with additional metrics reporting.
func (r *metricsRouter) Handle(pattern string, handler http.Handler) {
r.mux.Handle(pattern, ochttp.WithRouteTag(handler, pattern))
}
// HandleFunc is like (*http.ServeMux).HandleFunc but with additional metrics reporting.
func (r *metricsRouter) HandleFunc(pattern string, handler http.HandlerFunc) {
r.Handle(pattern, handler)
}
// ServeFiles serves files at the specified root with the
// Cache-Control header set to "no-cache, private, max-age=0".
func (r *metricsRouter) ServeFiles(pattern string, root fs.FS) {
fs := http.FileServerFS(root)
r.mux.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Cache-Control", "no-cache, private, max-age=0")
fs.ServeHTTP(w, req)
})
}
// ServeHTTP implements http.Handler.
func (r *metricsRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
}
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, ' ')]
}