internal/middleware: instrument cache latency

To prove the hypothesis that our cache timeout is too short, instrument
a cache latency metric.

Change-Id: Ie120f890b9dd488cdbb02556de84c9489ff8d846
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/247317
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index e8d32b8..d498d2e 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -138,6 +138,7 @@
 		frontend.VersionTypeCount,
 		middleware.CacheResultCount,
 		middleware.CacheErrorCount,
+		middleware.CacheLatency,
 		middleware.QuotaResultCount,
 	)
 	if err := dcensus.Init(cfg, views...); err != nil {
diff --git a/internal/middleware/caching.go b/internal/middleware/caching.go
index b8ec1da..7aaa1fb 100644
--- a/internal/middleware/caching.go
+++ b/internal/middleware/caching.go
@@ -15,6 +15,7 @@
 	"time"
 
 	"github.com/go-redis/redis/v7"
+	"go.opencensus.io/plugin/ochttp"
 	"go.opencensus.io/stats"
 	"go.opencensus.io/stats/view"
 	"go.opencensus.io/tag"
@@ -31,13 +32,17 @@
 		"The result of a cache request.",
 		stats.UnitDimensionless,
 	)
+	cacheLatency = stats.Float64(
+		"go-discovery/cache/result_latency",
+		"Cache serving latency latency",
+		stats.UnitMilliseconds,
+	)
 	cacheErrors = stats.Int64(
 		"go-discovery/cache/errors",
 		"Errors retrieving from cache.",
 		stats.UnitDimensionless,
 	)
 
-	// CacheResultCount is a counter of cache results, by cache name and hit success.
 	CacheResultCount = &view.View{
 		Name:        "go-discovery/cache/result_count",
 		Measure:     cacheResults,
@@ -45,7 +50,13 @@
 		Description: "cache results, by cache name and whether it was a hit",
 		TagKeys:     []tag.Key{keyCacheName, keyCacheHit},
 	}
-	// CacheErrorCount is a counter of cache errors, by cache name.
+	CacheLatency = &view.View{
+		Name:        "go-discovery/cache/result_latency",
+		Measure:     cacheLatency,
+		Description: "cache result latency, by cache name and whether it was a hit",
+		Aggregation: ochttp.DefaultLatencyDistribution,
+		TagKeys:     []tag.Key{keyCacheName, keyCacheHit},
+	}
 	CacheErrorCount = &view.View{
 		Name:        "go-discovery/cache/errors",
 		Measure:     cacheErrors,
@@ -59,11 +70,12 @@
 	testMode = false
 )
 
-func recordCacheResult(ctx context.Context, name string, hit bool) {
+func recordCacheResult(ctx context.Context, name string, hit bool, latency time.Duration) {
+	ms := float64(latency) / float64(time.Millisecond)
 	stats.RecordWithTags(ctx, []tag.Mutator{
 		tag.Upsert(keyCacheName, name),
 		tag.Upsert(keyCacheHit, strconv.FormatBool(hit)),
-	}, cacheResults.M(1))
+	}, cacheResults.M(1), cacheLatency.M(ms))
 }
 
 func recordCacheError(ctx context.Context, name, operation string) {
@@ -111,6 +123,7 @@
 }
 
 func (c *cache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	start := time.Now()
 	// Check auth header to see if request should bypass cache.
 	authVal := r.Header.Get(config.AuthHeader)
 	for _, wantVal := range c.authValues {
@@ -122,13 +135,13 @@
 	ctx := r.Context()
 	key := r.URL.String()
 	if reader, ok := c.get(ctx, key); ok {
-		recordCacheResult(ctx, c.name, true)
+		recordCacheResult(ctx, c.name, true, time.Since(start))
 		if _, err := io.Copy(w, reader); err != nil {
 			log.Errorf(ctx, "error copying zip bytes: %v", err)
 		}
 		return
 	}
-	recordCacheResult(ctx, c.name, false)
+	recordCacheResult(ctx, c.name, false, time.Since(start))
 	rec := newRecorder(w)
 	c.delegate.ServeHTTP(rec, r)
 	if rec.bufErr == nil && (rec.statusCode == 0 || rec.statusCode == http.StatusOK) {