internal/middleware: move stats to its own package

Make a package internal/middleware/stats for middleware.Stats and
middleware.ElapsedStat. This is part of removing the dependency from
internal/frontend on internal/middleware.

For golang/go#61399

Change-Id: I44afbfc9b9e28e1caabab8fe700376ec026c863d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/514521
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
kokoro-CI: kokoro <noreply+kokoro@google.com>
diff --git a/internal/frontend/details.go b/internal/frontend/details.go
index 13463fd..90e5af5 100644
--- a/internal/frontend/details.go
+++ b/internal/frontend/details.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 	"errors"
+	mstats "golang.org/x/pkgsite/internal/middleware/stats"
 	"net/http"
 	"strings"
 
@@ -16,7 +17,6 @@
 	"go.opencensus.io/tag"
 	"golang.org/x/mod/semver"
 	"golang.org/x/pkgsite/internal"
-	"golang.org/x/pkgsite/internal/middleware"
 	"golang.org/x/pkgsite/internal/stdlib"
 )
 
@@ -25,7 +25,7 @@
 // stdlib module pages are handled at "/std", and requests to "/mod/std" will
 // be redirected to that path.
 func (s *Server) serveDetails(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
-	defer middleware.ElapsedStat(r.Context(), "serveDetails")()
+	defer mstats.Elapsed(r.Context(), "serveDetails")()
 
 	ctx := r.Context()
 	if r.Method != http.MethodGet && r.Method != http.MethodHead {
diff --git a/internal/frontend/doc.go b/internal/frontend/doc.go
index ba53ca0..82b4e91 100644
--- a/internal/frontend/doc.go
+++ b/internal/frontend/doc.go
@@ -16,14 +16,14 @@
 	"golang.org/x/pkgsite/internal/godoc"
 	"golang.org/x/pkgsite/internal/godoc/dochtml"
 	"golang.org/x/pkgsite/internal/log"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/stdlib"
 )
 
 func renderDocParts(ctx context.Context, u *internal.Unit, docPkg *godoc.Package,
 	nameToVersion map[string]string, bc internal.BuildContext) (_ *dochtml.Parts, err error) {
 	defer derrors.Wrap(&err, "renderDocParts")
-	defer middleware.ElapsedStat(ctx, "renderDocParts")()
+	defer stats.Elapsed(ctx, "renderDocParts")()
 
 	modInfo := &godoc.ModuleInfo{
 		ModulePath:      u.ModulePath,
diff --git a/internal/frontend/latest_version.go b/internal/frontend/latest_version.go
index e3e4b73..a98e61c 100644
--- a/internal/frontend/latest_version.go
+++ b/internal/frontend/latest_version.go
@@ -9,7 +9,7 @@
 
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/log"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 )
 
 // GetLatestInfo returns various pieces of information about the latest
@@ -21,7 +21,7 @@
 // It returns empty strings on error.
 // It is intended to be used as an argument to middleware.LatestVersions.
 func (s *Server) GetLatestInfo(ctx context.Context, unitPath, modulePath string, latestUnitMeta *internal.UnitMeta) internal.LatestInfo {
-	defer middleware.ElapsedStat(ctx, "GetLatestInfo")()
+	defer stats.Elapsed(ctx, "GetLatestInfo")()
 
 	// It is okay to use a different DataSource (DB connection) than the rest of the
 	// request, because this makes self-contained calls on the DB.
diff --git a/internal/frontend/main.go b/internal/frontend/main.go
index fe3792c..d5f7689 100644
--- a/internal/frontend/main.go
+++ b/internal/frontend/main.go
@@ -17,6 +17,7 @@
 	"golang.org/x/pkgsite/internal/godoc/dochtml"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/version"
 	"golang.org/x/text/message"
 )
@@ -107,7 +108,7 @@
 
 func fetchMainDetails(ctx context.Context, ds internal.DataSource, um *internal.UnitMeta,
 	requestedVersion string, expandReadme bool, bc internal.BuildContext) (_ *MainDetails, err error) {
-	defer middleware.ElapsedStat(ctx, "fetchMainDetails")()
+	defer stats.Elapsed(ctx, "fetchMainDetails")()
 
 	unit, err := ds.GetUnit(ctx, um, internal.WithMain, bc)
 	if err != nil {
@@ -146,7 +147,7 @@
 		goos = doc.GOOS
 		goarch = doc.GOARCH
 		buildContexts = unit.BuildContexts
-		end := middleware.ElapsedStat(ctx, "DecodePackage")
+		end := stats.Elapsed(ctx, "DecodePackage")
 		docPkg, err := godoc.DecodePackage(doc.Source)
 		end()
 		if err != nil {
@@ -167,7 +168,7 @@
 		for _, l := range docParts.Links {
 			docLinks = append(docLinks, link{Href: l.Href, Body: l.Text})
 		}
-		end = middleware.ElapsedStat(ctx, "sourceFiles")
+		end = stats.Elapsed(ctx, "sourceFiles")
 		files = sourceFiles(unit, docPkg)
 		end()
 	}
@@ -253,7 +254,7 @@
 // into an outline.
 func readmeContent(ctx context.Context, u *internal.Unit) (_ *Readme, err error) {
 	defer derrors.Wrap(&err, "readmeContent(%q, %q, %q)", u.Path, u.ModulePath, u.Version)
-	defer middleware.ElapsedStat(ctx, "readmeContent")()
+	defer stats.Elapsed(ctx, "readmeContent")()
 	if !u.IsRedistributable {
 		return &Readme{}, nil
 	}
diff --git a/internal/frontend/server.go b/internal/frontend/server.go
index 4ec4ca5..7fcdf40 100644
--- a/internal/frontend/server.go
+++ b/internal/frontend/server.go
@@ -32,7 +32,7 @@
 	"golang.org/x/pkgsite/internal/licenses"
 	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/memory"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/queue"
 	"golang.org/x/pkgsite/internal/static"
 	"golang.org/x/pkgsite/internal/version"
@@ -198,9 +198,9 @@
 	handle("/", detailHandler)
 	if s.serveStats {
 		handle("/detail-stats/",
-			middleware.Stats()(http.StripPrefix("/detail-stats", s.errorHandler(s.serveDetails))))
+			stats.Stats()(http.StripPrefix("/detail-stats", s.errorHandler(s.serveDetails))))
 		handle("/search-stats/",
-			middleware.Stats()(http.StripPrefix("/search-stats", s.errorHandler(s.serveSearch))))
+			stats.Stats()(http.StripPrefix("/search-stats", s.errorHandler(s.serveSearch))))
 	}
 	handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
@@ -672,7 +672,7 @@
 
 // servePage is used to execute all templates for a *Server.
 func (s *Server) servePage(ctx context.Context, w http.ResponseWriter, templateName string, page any) {
-	defer middleware.ElapsedStat(ctx, "servePage")()
+	defer stats.Elapsed(ctx, "servePage")()
 
 	buf, err := s.renderPage(ctx, templateName, page)
 	if err != nil {
@@ -688,7 +688,7 @@
 
 // renderPage executes the given templateName with page.
 func (s *Server) renderPage(ctx context.Context, templateName string, page any) ([]byte, error) {
-	defer middleware.ElapsedStat(ctx, "renderPage")()
+	defer stats.Elapsed(ctx, "renderPage")()
 
 	tmpl, err := s.findTemplate(templateName)
 	if err != nil {
diff --git a/internal/frontend/unit.go b/internal/frontend/unit.go
index af2f5bd..849375c 100644
--- a/internal/frontend/unit.go
+++ b/internal/frontend/unit.go
@@ -18,7 +18,7 @@
 	"golang.org/x/pkgsite/internal/cookie"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/log"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/stdlib"
 	"golang.org/x/pkgsite/internal/version"
 	"golang.org/x/pkgsite/internal/vuln"
@@ -106,7 +106,7 @@
 func (s *Server) serveUnitPage(ctx context.Context, w http.ResponseWriter, r *http.Request,
 	ds internal.DataSource, info *urlPathInfo) (err error) {
 	defer derrors.Wrap(&err, "serveUnitPage(ctx, w, r, ds, %v)", info)
-	defer middleware.ElapsedStat(ctx, "serveUnitPage")()
+	defer stats.Elapsed(ctx, "serveUnitPage")()
 
 	tab := r.FormValue("tab")
 	if tab == "" {
diff --git a/internal/middleware/stats.go b/internal/middleware/stats/stats.go
similarity index 88%
rename from internal/middleware/stats.go
rename to internal/middleware/stats/stats.go
index 1a93987..a244385 100644
--- a/internal/middleware/stats.go
+++ b/internal/middleware/stats/stats.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package middleware
+package stats
 
 import (
 	"context"
@@ -18,7 +18,7 @@
 
 // Stats returns a Middleware that, instead of serving the page,
 // serves statistics about the page.
-func Stats() Middleware {
+func Stats() func(http.Handler) http.Handler {
 	return func(h http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			sw := newStatsResponseWriter()
@@ -29,9 +29,9 @@
 	}
 }
 
-// SetStat sets a stat named key in the current context. If key already has a
+// set sets a stat named key in the current context. If key already has a
 // value, the old and new value are both stored in a slice.
-func SetStat(ctx context.Context, key string, value any) {
+func set(ctx context.Context, key string, value any) {
 	x := ctx.Value(statsKey{})
 	if x == nil {
 		return
@@ -47,17 +47,17 @@
 	}
 }
 
-// ElapsedStat records as a stat the elapsed time for a
+// Elapsed records as a stat the elapsed time for a
 // function execution. Invoke like so:
 //
-//	defer ElapsedStat(ctx, "FunctionName")()
+//	defer Elapsed(ctx, "FunctionName")()
 //
 // The resulting stat will be called "FunctionName ms" and will
 // be the wall-clock execution time of the function in milliseconds.
-func ElapsedStat(ctx context.Context, name string) func() {
+func Elapsed(ctx context.Context, name string) func() {
 	start := time.Now()
 	return func() {
-		SetStat(ctx, name+" ms", time.Since(start).Milliseconds())
+		set(ctx, name+" ms", time.Since(start).Milliseconds())
 	}
 }
 
diff --git a/internal/middleware/stats_test.go b/internal/middleware/stats/stats_test.go
similarity index 95%
rename from internal/middleware/stats_test.go
rename to internal/middleware/stats/stats_test.go
index 74b6170..39fb5f9 100644
--- a/internal/middleware/stats_test.go
+++ b/internal/middleware/stats/stats_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package middleware
+package stats
 
 import (
 	"encoding/json"
@@ -23,11 +23,11 @@
 	ts := httptest.NewServer(Stats()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		ctx := r.Context()
 		w.WriteHeader(code)
-		SetStat(ctx, "a", 1)
+		set(ctx, "a", 1)
 		w.Write(data[:10])
-		SetStat(ctx, "b", 2)
+		set(ctx, "b", 2)
 		time.Sleep(500 * time.Millisecond)
-		SetStat(ctx, "a", 3)
+		set(ctx, "a", 3)
 		w.Write(data[10:])
 	})))
 	defer ts.Close()
diff --git a/internal/postgres/details.go b/internal/postgres/details.go
index 0e67d42..871dc0e 100644
--- a/internal/postgres/details.go
+++ b/internal/postgres/details.go
@@ -15,14 +15,14 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 )
 
 // GetNestedModules returns the latest major version of all nested modules
 // given a modulePath path prefix with or without major version.
 func (db *DB) GetNestedModules(ctx context.Context, modulePath string) (_ []*internal.ModuleInfo, err error) {
 	defer derrors.WrapStack(&err, "GetNestedModules(ctx, %v)", modulePath)
-	defer middleware.ElapsedStat(ctx, "GetNestedModules")()
+	defer stats.Elapsed(ctx, "GetNestedModules")()
 
 	query := `
 		SELECT DISTINCT ON (series_path)
@@ -78,7 +78,7 @@
 // Instead of supporting pagination, this query runs with a limit.
 func (db *DB) GetImportedBy(ctx context.Context, pkgPath, modulePath string, limit int) (paths []string, err error) {
 	defer derrors.WrapStack(&err, "GetImportedBy(ctx, %q, %q)", pkgPath, modulePath)
-	defer middleware.ElapsedStat(ctx, "GetImportedBy")()
+	defer stats.Elapsed(ctx, "GetImportedBy")()
 
 	if pkgPath == "" {
 		return nil, fmt.Errorf("pkgPath cannot be empty: %w", derrors.InvalidArgument)
@@ -102,7 +102,7 @@
 // GetImportedByCount returns the number of packages that import pkgPath.
 func (db *DB) GetImportedByCount(ctx context.Context, pkgPath, modulePath string) (_ int, err error) {
 	defer derrors.WrapStack(&err, "GetImportedByCount(ctx, %q, %q)", pkgPath, modulePath)
-	defer middleware.ElapsedStat(ctx, "GetImportedByCount")()
+	defer stats.Elapsed(ctx, "GetImportedByCount")()
 
 	if pkgPath == "" {
 		return 0, fmt.Errorf("pkgPath cannot be empty: %w", derrors.InvalidArgument)
diff --git a/internal/postgres/licenses.go b/internal/postgres/licenses.go
index a728220..865b3fc 100644
--- a/internal/postgres/licenses.go
+++ b/internal/postgres/licenses.go
@@ -17,13 +17,13 @@
 	"github.com/lib/pq"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/licenses"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/stdlib"
 )
 
 func (db *DB) getLicenses(ctx context.Context, fullPath, modulePath string, unitID int) (_ []*licenses.License, err error) {
 	defer derrors.WrapStack(&err, "getLicenses(ctx, %d)", unitID)
-	defer middleware.ElapsedStat(ctx, "getLicenses")()
+	defer stats.Elapsed(ctx, "getLicenses")()
 
 	query := `
 		SELECT
diff --git a/internal/postgres/package_symbol.go b/internal/postgres/package_symbol.go
index 5e16204..aaa2826 100644
--- a/internal/postgres/package_symbol.go
+++ b/internal/postgres/package_symbol.go
@@ -13,14 +13,14 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 )
 
 // getPackageSymbols returns all of the symbols for a given package path and module path.
 func getPackageSymbols(ctx context.Context, ddb *database.DB, packagePath, modulePath string,
 ) (_ *internal.SymbolHistory, err error) {
 	defer derrors.Wrap(&err, "getPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
-	defer middleware.ElapsedStat(ctx, "getPackageSymbols")()
+	defer stats.Elapsed(ctx, "getPackageSymbols")()
 
 	query := packageSymbolQueryJoin(
 		squirrel.Select(
diff --git a/internal/postgres/symbol_history.go b/internal/postgres/symbol_history.go
index 8bb51cd..36daa99 100644
--- a/internal/postgres/symbol_history.go
+++ b/internal/postgres/symbol_history.go
@@ -13,7 +13,7 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/symbol"
 )
 
@@ -22,7 +22,7 @@
 func (db *DB) GetSymbolHistory(ctx context.Context, packagePath, modulePath string,
 ) (_ *internal.SymbolHistory, err error) {
 	defer derrors.Wrap(&err, "GetSymbolHistory(ctx, %q, %q)", packagePath, modulePath)
-	defer middleware.ElapsedStat(ctx, "GetSymbolHistory")()
+	defer stats.Elapsed(ctx, "GetSymbolHistory")()
 
 	return GetSymbolHistoryFromTable(ctx, db.db, packagePath, modulePath)
 }
@@ -71,7 +71,7 @@
 func GetSymbolHistoryWithPackageSymbols(ctx context.Context, ddb *database.DB,
 	packagePath, modulePath string) (_ *internal.SymbolHistory, err error) {
 	defer derrors.WrapStack(&err, "GetSymbolHistoryWithPackageSymbols(ctx, ddb, %q, %q)", packagePath, modulePath)
-	defer middleware.ElapsedStat(ctx, "GetSymbolHistoryWithPackageSymbols")()
+	defer stats.Elapsed(ctx, "GetSymbolHistoryWithPackageSymbols")()
 	sh, err := getPackageSymbols(ctx, ddb, packagePath, modulePath)
 	if err != nil {
 		return nil, err
@@ -86,7 +86,7 @@
 func GetSymbolHistoryForBuildContext(ctx context.Context, ddb *database.DB, pathID int, modulePath string,
 	bc internal.BuildContext) (_ map[string]string, err error) {
 	defer derrors.WrapStack(&err, "GetSymbolHistoryForBuildContext(ctx, ddb, %d, %q)", pathID, modulePath)
-	defer middleware.ElapsedStat(ctx, "GetSymbolHistoryForBuildContext")()
+	defer stats.Elapsed(ctx, "GetSymbolHistoryForBuildContext")()
 
 	if bc == internal.BuildContextAll {
 		bc = internal.BuildContextLinux
diff --git a/internal/postgres/symbolsearch.go b/internal/postgres/symbolsearch.go
index a89118a..e50cc11 100644
--- a/internal/postgres/symbolsearch.go
+++ b/internal/postgres/symbolsearch.go
@@ -15,7 +15,7 @@
 	"github.com/lib/pq"
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/postgres/search"
 	"golang.org/x/sync/errgroup"
 )
@@ -23,7 +23,7 @@
 func upsertSymbolSearchDocuments(ctx context.Context, tx *database.DB,
 	modulePath, v string) (err error) {
 	defer derrors.Wrap(&err, "upsertSymbolSearchDocuments(ctx, ddb, %q, %q)", modulePath, v)
-	defer middleware.ElapsedStat(ctx, "upsertSymbolSearchDocuments")()
+	defer stats.Elapsed(ctx, "upsertSymbolSearchDocuments")()
 
 	// If a user is looking for the symbol "DB.Begin", from package
 	// database/sql, we want them to be able to find this by searching for
@@ -97,7 +97,7 @@
 // TODO(https://golang.org/issue/44142): factor out common code between
 // symbolSearch and deepSearch.
 func (db *DB) symbolSearch(ctx context.Context, q string, limit int, opts SearchOptions) searchResponse {
-	defer middleware.ElapsedStat(ctx, "symbolSearch")()
+	defer stats.Elapsed(ctx, "symbolSearch")()
 
 	var (
 		results []*SearchResult
@@ -156,7 +156,7 @@
 	symbolFilter string) (_ []*SearchResult, err error) {
 	defer derrors.Wrap(&err, "runSymbolSearchMultiWord(ctx, ddb, query, %q, %d, %q)",
 		q, limit, symbolFilter)
-	defer middleware.ElapsedStat(ctx, "runSymbolSearchMultiWord")()
+	defer stats.Elapsed(ctx, "runSymbolSearchMultiWord")()
 
 	symbolToPathTokens := multiwordSearchCombinations(q, symbolFilter)
 	if len(symbolToPathTokens) == 0 {
@@ -259,7 +259,7 @@
 // when using an OR in the WHERE clause.
 func runSymbolSearchOneDot(ctx context.Context, ddb *database.DB, q string, limit int) (_ []*SearchResult, err error) {
 	defer derrors.Wrap(&err, "runSymbolSearchOneDot(ctx, ddb, %q, %d)", q, limit)
-	defer middleware.ElapsedStat(ctx, "runSymbolSearchOneDot")()
+	defer stats.Elapsed(ctx, "runSymbolSearchOneDot")()
 
 	group, searchCtx := errgroup.WithContext(ctx)
 	resultsArray := make([][]*SearchResult, 2)
@@ -318,7 +318,7 @@
 func runSymbolSearch(ctx context.Context, ddb *database.DB,
 	st search.SearchType, q string, limit int, args ...any) (results []*SearchResult, err error) {
 	defer derrors.Wrap(&err, "runSymbolSearch(ctx, ddb, %q, %q, %d, %v)", st, q, limit, args)
-	defer middleware.ElapsedStat(ctx, fmt.Sprintf("%s-runSymbolSearch", st))()
+	defer stats.Elapsed(ctx, fmt.Sprintf("%s-runSymbolSearch", st))()
 
 	collect := func(rows *sql.Rows) error {
 		var r SearchResult
diff --git a/internal/postgres/unit.go b/internal/postgres/unit.go
index e961358..3879352 100644
--- a/internal/postgres/unit.go
+++ b/internal/postgres/unit.go
@@ -16,7 +16,7 @@
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/stdlib"
 	"golang.org/x/pkgsite/internal/version"
 )
@@ -39,7 +39,7 @@
 //	   we do have: again first by module path length, then by version.
 func (db *DB) GetUnitMeta(ctx context.Context, fullPath, requestedModulePath, requestedVersion string) (_ *internal.UnitMeta, err error) {
 	defer derrors.WrapStack(&err, "DB.GetUnitMeta(ctx, %q, %q, %q)", fullPath, requestedModulePath, requestedVersion)
-	defer middleware.ElapsedStat(ctx, "DB.GetUnitMeta")()
+	defer stats.Elapsed(ctx, "DB.GetUnitMeta")()
 
 	modulePath := requestedModulePath
 	v := requestedVersion
@@ -55,7 +55,7 @@
 
 func (db *DB) getUnitMetaWithKnownVersion(ctx context.Context, fullPath, modulePath, version string, lmv *internal.LatestModuleVersions) (_ *internal.UnitMeta, err error) {
 	defer derrors.WrapStack(&err, "getUnitMetaWithKnownVersion")
-	defer middleware.ElapsedStat(ctx, "getUnitMetaWithKnownVersion")()
+	defer stats.Elapsed(ctx, "getUnitMetaWithKnownVersion")()
 
 	query := squirrel.Select(
 		"m.module_path",
@@ -146,7 +146,7 @@
 	modulePath, latestVersion string, lmv *internal.LatestModuleVersions, err error) {
 
 	defer derrors.WrapStack(&err, "getLatestUnitVersion(%q, %q)", fullPath, requestedModulePath)
-	defer middleware.ElapsedStat(ctx, "getLatestUnitVersion")()
+	defer stats.Elapsed(ctx, "getLatestUnitVersion")()
 
 	modPaths := []string{requestedModulePath}
 	// If we don't know the module path, try each possible module path from longest to shortest.
@@ -259,7 +259,7 @@
 		return u, nil
 	}
 
-	defer middleware.ElapsedStat(ctx, "GetUnit")()
+	defer stats.Elapsed(ctx, "GetUnit")()
 	unitID, err := db.getUnitID(ctx, um.Path, um.ModulePath, um.Version)
 	if err != nil {
 		return nil, err
@@ -292,7 +292,7 @@
 
 func (db *DB) getUnitID(ctx context.Context, fullPath, modulePath, resolvedVersion string) (_ int, err error) {
 	defer derrors.WrapStack(&err, "getUnitID(ctx, %q, %q, %q)", fullPath, modulePath, resolvedVersion)
-	defer middleware.ElapsedStat(ctx, "getUnitID")()
+	defer stats.Elapsed(ctx, "getUnitID")()
 	var unitID int
 	query := `
 		SELECT u.id
@@ -317,7 +317,7 @@
 // getImports returns the imports corresponding to unitID.
 func (db *DB) getImports(ctx context.Context, unitID int) (_ []string, err error) {
 	defer derrors.WrapStack(&err, "getImports(ctx, %d)", unitID)
-	defer middleware.ElapsedStat(ctx, "getImports")()
+	defer stats.Elapsed(ctx, "getImports")()
 	query := `
 		SELECT p.path
 		FROM paths p INNER JOIN imports i ON p.id = i.to_path_id
@@ -333,7 +333,7 @@
 
 func getPackagesInUnit(ctx context.Context, db *database.DB, fullPath, modulePath, resolvedVersion string, moduleID int, bypassLicenseCheck bool) (_ []*internal.PackageMeta, err error) {
 	defer derrors.WrapStack(&err, "getPackagesInUnit(ctx, %q, %q, %q, %d)", fullPath, modulePath, resolvedVersion, moduleID)
-	defer middleware.ElapsedStat(ctx, "getPackagesInUnit")()
+	defer stats.Elapsed(ctx, "getPackagesInUnit")()
 
 	queryBuilder := squirrel.Select(
 		"p.path",
@@ -427,7 +427,7 @@
 
 func (db *DB) getUnitWithAllFields(ctx context.Context, um *internal.UnitMeta, bc internal.BuildContext) (_ *internal.Unit, err error) {
 	defer derrors.WrapStack(&err, "getUnitWithAllFields(ctx, %q, %q, %q)", um.Path, um.ModulePath, um.Version)
-	defer middleware.ElapsedStat(ctx, "getUnitWithAllFields")()
+	defer stats.Elapsed(ctx, "getUnitWithAllFields")()
 
 	// Get build contexts and unit ID.
 	var pathID, unitID, moduleID int
@@ -509,7 +509,7 @@
 		goarch = bcMatched.GOARCH
 	}
 	doc := &internal.Documentation{GOOS: bcMatched.GOOS, GOARCH: bcMatched.GOARCH}
-	end := middleware.ElapsedStat(ctx, "getUnitWithAllFields-readme-and-imports")
+	end := stats.Elapsed(ctx, "getUnitWithAllFields-readme-and-imports")
 	err = db.db.QueryRow(ctx, query, pathID, unitID, goos, goarch).Scan(
 		database.NullIsEmpty(&r.Filepath),
 		database.NullIsEmpty(&r.Contents),
diff --git a/internal/postgres/version.go b/internal/postgres/version.go
index ae116e7..0041531 100644
--- a/internal/postgres/version.go
+++ b/internal/postgres/version.go
@@ -18,7 +18,7 @@
 	"golang.org/x/pkgsite/internal/database"
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/log"
-	"golang.org/x/pkgsite/internal/middleware"
+	"golang.org/x/pkgsite/internal/middleware/stats"
 	"golang.org/x/pkgsite/internal/version"
 	"golang.org/x/sync/errgroup"
 )
@@ -28,7 +28,7 @@
 // recent from a list of pseudo-versions sorted in descending semver order.
 func (db *DB) GetVersionsForPath(ctx context.Context, path string) (_ []*internal.ModuleInfo, err error) {
 	defer derrors.WrapStack(&err, "GetVersionsForPath(ctx, %q)", path)
-	defer middleware.ElapsedStat(ctx, "GetVersionsForPath")()
+	defer stats.Elapsed(ctx, "GetVersionsForPath")()
 
 	versions, err := getPathVersions(ctx, db, path, version.TypeRelease, version.TypePrerelease)
 	if err != nil {
@@ -163,7 +163,7 @@
 // That can save a redundant call to GetUnitMeta here.
 func (db *DB) GetLatestInfo(ctx context.Context, unitPath, modulePath string, latestUnitMeta *internal.UnitMeta) (latest internal.LatestInfo, err error) {
 	defer derrors.WrapStack(&err, "DB.GetLatestInfo(ctx, %q, %q)", unitPath, modulePath)
-	defer middleware.ElapsedStat(ctx, "DB.GetLatestInfo")()
+	defer stats.Elapsed(ctx, "DB.GetLatestInfo")()
 
 	group, gctx := errgroup.WithContext(ctx)
 
@@ -208,7 +208,7 @@
 // it returns empty strings.
 func (db *DB) getLatestMajorVersion(ctx context.Context, fullPath, modulePath string) (modPath, pkgPath string, err error) {
 	defer derrors.WrapStack(&err, "DB.getLatestMajorVersion2(%q)", modulePath)
-	defer middleware.ElapsedStat(ctx, "DB.getLatestMajorVersion")()
+	defer stats.Elapsed(ctx, "DB.getLatestMajorVersion")()
 
 	// Collect all the non-deprecated module paths for the series that have at
 	// least one good version, along with that good version. A good version
@@ -286,7 +286,7 @@
 // unitExistsAtLatest reports whether unitPath exists at the latest version of modulePath.
 func (db *DB) unitExistsAtLatest(ctx context.Context, unitPath, modulePath string) (unitExists bool, err error) {
 	defer derrors.WrapStack(&err, "DB.unitExistsAtLatest(ctx, %q, %q)", unitPath, modulePath)
-	defer middleware.ElapsedStat(ctx, "DB.unitExistsAtLatest")()
+	defer stats.Elapsed(ctx, "DB.unitExistsAtLatest")()
 
 	// Find the latest version of the module path in the modules table.
 	var latestGoodVersion string
@@ -336,7 +336,7 @@
 
 func (db *DB) getMultiLatestModuleVersions(ctx context.Context, modulePaths []string) (lmvs []*internal.LatestModuleVersions, err error) {
 	defer derrors.WrapStack(&err, "getMultiLatestModuleVersions(%v)", modulePaths)
-	defer middleware.ElapsedStat(ctx, "getMultiLatestModuleVersions")()
+	defer stats.Elapsed(ctx, "getMultiLatestModuleVersions")()
 
 	collect := func(rows *sql.Rows) error {
 		var (