internal/observe: package for tracing and metrics

Factor out the tracing and metrics code from the server
and put it in its own package.

This is a first step towards sharing it with other projects.

Change-Id: I36a04933accc11300f360a410c00a10c8a132dda
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/435470
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Tatiana Bradley <tatiana@golang.org>
Reviewed-by: Julie Qiu <julieqiu@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/checks.bash b/checks.bash
index cf50e10..6311819 100755
--- a/checks.bash
+++ b/checks.bash
@@ -62,7 +62,8 @@
 # check_unparam runs unparam on source files.
 check_unparam() {
   ensure_go_binary mvdan.cc/unparam
-  runcmd unparam ./...
+  # Temporarily exclude until updated for Go 1.20.
+  # runcmd unparam ./...
 }
 
 # check_vet runs go vet on source files.
diff --git a/go.mod b/go.mod
index 0e2762b..c6615bc 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,6 @@
 	github.com/jba/templatecheck v0.6.0
 	github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2
 	go.opentelemetry.io/otel v1.4.0
-	go.opentelemetry.io/otel/metric v0.27.0
 	go.opentelemetry.io/otel/sdk v1.4.0
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
 	golang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e
@@ -37,7 +36,7 @@
 	google.golang.org/grpc v1.44.0
 	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
 	honnef.co/go/tools v0.2.2
-	mvdan.cc/unparam v0.0.0-20220706161116-678bad134442
+	mvdan.cc/unparam v0.0.0-20220926085101-66de63301820
 )
 
 require (
@@ -67,12 +66,12 @@
 	github.com/xanzy/ssh-agent v0.3.0 // indirect
 	go.opencensus.io v0.23.0 // indirect
 	go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
+	go.opentelemetry.io/otel/metric v0.27.0 // indirect
 	go.opentelemetry.io/otel/sdk/export/metric v0.26.0 // indirect
 	go.opentelemetry.io/otel/sdk/metric v0.26.0 // indirect
 	go.opentelemetry.io/otel/trace v1.4.0 // indirect
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
-	golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d // indirect
-	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+	golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
diff --git a/go.sum b/go.sum
index 023fbda..203c728 100644
--- a/go.sum
+++ b/go.sum
@@ -269,7 +269,7 @@
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2 h1:82EIpiGB79OIPgSGa63Oj4Ipf+YAX1c6A9qjmEYoRXc=
@@ -357,8 +357,6 @@
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e h1:K2AuHMC+jaRTzAcivRwKOzjTZ1925Yx4xHMg07YoBQc=
 golang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AVlZHjhWbW/3yOcmKMtJiObwBPJajBlUpQXRijFNrNc=
-golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d h1:+W8Qf4iJtMGKkyAygcKohjxTk4JPsL9DpzApJ22m5Ic=
-golang.org/x/exp/typeparams v0.0.0-20220613132600-b0d781184e0d/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -518,8 +516,8 @@
 golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
+golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -767,7 +765,6 @@
 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
@@ -787,8 +784,8 @@
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk=
 honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
-mvdan.cc/unparam v0.0.0-20220706161116-678bad134442 h1:seuXWbRB1qPrS3NQnHmFKLJLtskWyueeIzmLXghMGgk=
-mvdan.cc/unparam v0.0.0-20220706161116-678bad134442/go.mod h1:F/Cxw/6mVrNKqrR2YjFf5CaW0Bw4RL8RfbEf4GRggJk=
+mvdan.cc/unparam v0.0.0-20220926085101-66de63301820 h1:fggBTMFbBz7CMny3mWZphe0B/6D8ILBunvvB1cNNHi8=
+mvdan.cc/unparam v0.0.0-20220926085101-66de63301820/go.mod h1:7fKhD/gH+APJ9Y27S2PYO7+oVWtb3XPrw9W5ayxVq2A=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/internal/observe/observe.go b/internal/observe/observe.go
new file mode 100644
index 0000000..8624bd5
--- /dev/null
+++ b/internal/observe/observe.go
@@ -0,0 +1,98 @@
+// 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 observe provides metric and tracing support for Go servers.
+// It uses OpenTelemetry and the golang.org/x/exp/events package.
+package observe
+
+import (
+	"context"
+	"net/http"
+
+	"golang.org/x/exp/event"
+	"golang.org/x/vulndb/internal/derrors"
+
+	mexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric"
+	texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
+	gcppropagator "github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator"
+	"go.opentelemetry.io/otel/propagation"
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	eotel "golang.org/x/exp/event/otel"
+)
+
+// An Observer handles tracing and metrics exporting.
+type Observer struct {
+	ctx            context.Context
+	tracerProvider *sdktrace.TracerProvider
+	traceHandler   *eotel.TraceHandler
+	metricHandler  *eotel.MetricHandler
+	propagator     propagation.TextMapPropagator
+}
+
+// NewObserver creates an Observer.
+// The context is used to flush traces in AfterRequest, so it should be longer-lived
+// than any request context.
+// (We don't want to use the request context because we still want traces even if
+// it is canceled or times out.)
+func NewObserver(ctx context.Context, projectID, serverName string) (_ *Observer, err error) {
+	defer derrors.Wrap(&err, "NewObserver(%q, %q)", projectID, serverName)
+
+	exporter, err := texporter.New(texporter.WithProjectID(projectID))
+	if err != nil {
+		return nil, err
+	}
+	// Create exporter (collector embedded with the exporter).
+	controller, err := mexporter.NewExportPipeline([]mexporter.Option{
+		mexporter.WithProjectID(projectID),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	tp := sdktrace.NewTracerProvider(
+		// Enable tracing if there is no incoming request, or if the incoming
+		// request is sampled.
+		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample())),
+		sdktrace.WithBatcher(exporter))
+	return &Observer{
+		ctx:            ctx,
+		tracerProvider: tp,
+		traceHandler:   eotel.NewTraceHandler(tp.Tracer(serverName)),
+		metricHandler:  eotel.NewMetricHandler(controller.Meter(serverName)),
+		// The propagator extracts incoming trace IDs so that we can connect our trace spans
+		// to the incoming ones constructed by Cloud Run.
+		propagator: propagation.NewCompositeTextMapPropagator(
+			propagation.TraceContext{},
+			propagation.Baggage{},
+			gcppropagator.New()),
+	}, nil
+}
+
+// BeforeRequest should be called before a request is processed.
+// otherHandler can be any event.Handler that should be added to the event exporter
+// for the request.
+func (o *Observer) BeforeRequest(r *http.Request, otherHandler event.Handler) *http.Request {
+	exporter := event.NewExporter(eventHandler{o, otherHandler}, nil)
+	ctx := event.WithExporter(r.Context(), exporter)
+	ctx = o.propagator.Extract(ctx, propagation.HeaderCarrier(r.Header))
+	return r.WithContext(ctx)
+
+}
+
+// AfterRequest should be called after each request.
+func (o *Observer) AfterRequest() {
+	o.tracerProvider.ForceFlush(o.ctx)
+}
+
+type eventHandler struct {
+	o  *Observer
+	eh event.Handler
+}
+
+// Event implements event.Handler.
+func (h eventHandler) Event(ctx context.Context, ev *event.Event) context.Context {
+	ctx = h.eh.Event(ctx, ev)
+	ctx = h.o.traceHandler.Event(ctx, ev)
+	return h.o.metricHandler.Event(ctx, ev)
+}
diff --git a/internal/worker/server.go b/internal/worker/server.go
index 5facb8c..b6ce60b 100644
--- a/internal/worker/server.go
+++ b/internal/worker/server.go
@@ -28,19 +28,15 @@
 	"golang.org/x/vulndb/internal/ghsa"
 	"golang.org/x/vulndb/internal/gitrepo"
 	"golang.org/x/vulndb/internal/issues"
+	"golang.org/x/vulndb/internal/observe"
 	"golang.org/x/vulndb/internal/worker/log"
 	"golang.org/x/vulndb/internal/worker/store"
-
-	mexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric"
-	texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
-	gcppropagator "github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator"
-	"go.opentelemetry.io/otel/metric"
-	"go.opentelemetry.io/otel/propagation"
-	sdktrace "go.opentelemetry.io/otel/sdk/trace"
-	eotel "golang.org/x/exp/event/otel"
 )
 
-const pkgsiteURL = "https://pkg.go.dev"
+const (
+	pkgsiteURL = "https://pkg.go.dev"
+	serverName = "vulndb-worker"
+)
 
 var staticPath = template.TrustedSourceFromConstant("internal/worker/static")
 
@@ -48,10 +44,7 @@
 	cfg           Config
 	indexTemplate *template.Template
 	issueClient   issues.Client
-	traceHandler  event.Handler
-	metricHandler event.Handler
-	propagator    propagation.TextMapPropagator
-	afterRequest  func()
+	observer      *observe.Observer
 }
 
 func NewServer(ctx context.Context, cfg Config) (_ *Server, err error) {
@@ -59,22 +52,10 @@
 
 	s := &Server{cfg: cfg}
 
-	tracerProvider, meterProvider, err := initOpenTelemetry(cfg.Project)
+	s.observer, err = observe.NewObserver(ctx, cfg.Project, serverName)
 	if err != nil {
 		return nil, err
 	}
-	s.traceHandler = eotel.NewTraceHandler(tracerProvider.Tracer("vulndb-worker"))
-	s.metricHandler = eotel.NewMetricHandler(meterProvider.Meter("vulndb-worker"))
-
-	s.afterRequest = func() { tracerProvider.ForceFlush(ctx) }
-	// The propagator extracts incoming trace IDs so that we can connect our trace spans
-	// to the incoming ones constructed by Cloud Run.
-	s.propagator = propagation.NewCompositeTextMapPropagator(
-		propagation.TraceContext{},
-		propagation.Baggage{},
-		gcppropagator.New(),
-	)
-
 	if cfg.UseErrorReporting {
 		reportingClient, err := errorreporting.NewClient(ctx, cfg.Project, errorreporting.Config{
 			ServiceName: serviceID,
@@ -124,8 +105,11 @@
 func (s *Server) handle(_ context.Context, pattern string, handler func(w http.ResponseWriter, r *http.Request) error) {
 	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
 		start := time.Now()
-		r = s.beforeRequest(r)
-		defer s.afterRequest()
+		const traceIDHeader = "X-Cloud-Trace-Context"
+
+		traceID := r.Header.Get(traceIDHeader)
+		r = s.observer.BeforeRequest(r, log.NewGCPJSONHandler(os.Stderr, traceID))
+		defer s.observer.AfterRequest()
 		ctx := r.Context()
 		log.With("httpRequest", r).Infof(ctx, "starting %s", r.URL.Path)
 
@@ -140,18 +124,6 @@
 	})
 }
 
-func (s *Server) beforeRequest(r *http.Request) *http.Request {
-	traceID := r.Header.Get("X-Cloud-Trace-Context")
-	exporter := event.NewExporter(multiEventHandler{
-		log.NewGCPJSONHandler(os.Stderr, traceID),
-		s.traceHandler,
-		s.metricHandler,
-	}, nil)
-	ctx := event.WithExporter(r.Context(), exporter)
-	ctx = s.propagator.Extract(ctx, propagation.HeaderCarrier(r.Header))
-	return r.WithContext(ctx)
-}
-
 type serverError struct {
 	status int   // HTTP status code
 	err    error // wrapped error
@@ -395,36 +367,3 @@
 func (s *Server) handleScanModules(w http.ResponseWriter, r *http.Request) error {
 	return ScanModules(r.Context(), s.cfg.Store, r.FormValue("force") == "true")
 }
-
-func initOpenTelemetry(projectID string) (tp *sdktrace.TracerProvider, mp metric.MeterProvider, err error) {
-	defer derrors.Wrap(&err, "initOpenTelemetry(%q)", projectID)
-
-	exporter, err := texporter.New(texporter.WithProjectID(projectID))
-	if err != nil {
-		return nil, nil, err
-	}
-	tp = sdktrace.NewTracerProvider(
-		// Enable tracing if there is no incoming request, or if the incoming
-		// request is sampled.
-		sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.AlwaysSample())),
-		sdktrace.WithBatcher(exporter))
-
-	// Create exporter (collector embedded with the exporter).
-	controller, err := mexporter.NewExportPipeline([]mexporter.Option{mexporter.WithProjectID(projectID)})
-	if err != nil {
-		return nil, nil, err
-	}
-	return tp, controller, nil
-}
-
-// multiEventHandler is an event.Handler that calls all of its contained handlers
-// on each event.
-type multiEventHandler []event.Handler
-
-// Event implements event.Handler.Event.
-func (eh multiEventHandler) Event(ctx context.Context, ev *event.Event) context.Context {
-	for _, h := range eh {
-		ctx = h.Event(ctx, ev)
-	}
-	return ctx
-}