internal/frontend: add instrumentation for frontend fetch

Instrumentation for frontend fetch requests is added via the ochttp and
ocsql integrations.

Change-Id: I095ec224c24bdb7e341f2b620ddf020ed50f556e
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/765147
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index d3b29fd..1125f8b 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -115,6 +115,8 @@
 	views := append(dcensus.ServerViews,
 		postgres.SearchLatencyDistribution,
 		postgres.SearchResponseCount,
+		frontend.FrontendFetchLatencyDistribution,
+		frontend.FrontendFetchResponseCount,
 		middleware.CacheResultCount,
 		middleware.CacheErrorCount,
 		middleware.QuotaResultCount,
diff --git a/internal/frontend/fetch.go b/internal/frontend/fetch.go
index 2d212e6..99de031 100644
--- a/internal/frontend/fetch.go
+++ b/internal/frontend/fetch.go
@@ -9,17 +9,20 @@
 	"errors"
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
 
+	"go.opencensus.io/plugin/ochttp"
+	"go.opencensus.io/stats"
+	"go.opencensus.io/stats/view"
+	"go.opencensus.io/tag"
 	"golang.org/x/mod/semver"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/fetch"
-
 	"golang.org/x/pkgsite/internal/experiment"
-
+	"golang.org/x/pkgsite/internal/fetch"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/proxy"
@@ -36,6 +39,33 @@
 	errPathDoesNotExistInModule = errors.New("path does not exist in module")
 	fetchTimeout                = 30 * time.Second
 	pollEvery                   = 500 * time.Millisecond
+
+	// keyFrontendFetchStatus is a census tag for frontend fetch query types.
+	keyFrontendFetchStatus = tag.MustNewKey("frontend-fetch.status")
+	// keyFrontendFetchLatency holds observed latency in individual
+	// frontend fetch queries.
+	keyFrontendFetchLatency = stats.Float64(
+		"go-discovery/frontend-fetch/latency",
+		"Latency of a frontend fetch request.",
+		stats.UnitMilliseconds,
+	)
+	// FrontendFetchLatencyDistribution aggregates frontend fetch request
+	// latency by status code.
+	FrontendFetchLatencyDistribution = &view.View{
+		Name:        "go-discovery/frontend-fetch/latency",
+		Measure:     keyFrontendFetchLatency,
+		Aggregation: ochttp.DefaultLatencyDistribution,
+		Description: "FrontendFetch latency, by result source query type.",
+		TagKeys:     []tag.Key{keyFrontendFetchStatus},
+	}
+	// FrontendFetchResponseCount counts frontend fetch responses by response type.
+	FrontendFetchResponseCount = &view.View{
+		Name:        "go-discovery/frontend-fetch/count",
+		Measure:     keyFrontendFetchLatency,
+		Aggregation: view.Count(),
+		Description: "Frontend fetch request count",
+		TagKeys:     []tag.Key{keyFrontendFetchStatus},
+	}
 )
 
 // fetchHandler checks if a requested path and version exists in the database.
@@ -65,7 +95,9 @@
 		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 		return
 	}
+	start := time.Now()
 	status, responseText := s.fetchAndPoll(r.Context(), modulePath, fullPath, requestedVersion)
+	recordFrontendFetchMetric(status, time.Since(start))
 	if status != http.StatusOK {
 		http.Error(w, responseText, status)
 		return
@@ -427,3 +459,10 @@
 	return experiment.IsActive(ctx, internal.ExperimentFrontendFetch) &&
 		experiment.IsActive(ctx, internal.ExperimentInsertDirectories)
 }
+
+func recordFrontendFetchMetric(status int, latency time.Duration) {
+	l := float64(latency) / float64(time.Millisecond)
+	stats.RecordWithTags(context.Background(), []tag.Mutator{
+		tag.Upsert(keyFrontendFetchStatus, strconv.Itoa(status)),
+	}, keyFrontendFetchLatency.M(l))
+}