all: add basic trace to gddo

This uses Stackdriver trace API to trace search, package requests
as well as background tasks. It also traces outbound requests to
VCS's and remote_api calls.

Change-Id: Ice82c1fbd267eefa4bad9d6674dc39ef10a1c874
Reviewed-on: https://go-review.googlesource.com/68472
Reviewed-by: Ross Light <light@google.com>
diff --git a/database/database.go b/database/database.go
index 4f112fb..b41392f 100644
--- a/database/database.go
+++ b/database/database.go
@@ -46,6 +46,7 @@
 	"time"
 	"unicode/utf8"
 
+	"cloud.google.com/go/trace"
 	"github.com/garyburd/redigo/redis"
 	"github.com/golang/snappy"
 	"golang.org/x/oauth2/google"
@@ -128,6 +129,7 @@
 	if err != nil {
 		return nil, err
 	}
+	client.Transport = trace.Transport{Base: client.Transport}
 
 	return remote_api.NewClient(host, client)
 }
diff --git a/gddo-server/background.go b/gddo-server/background.go
index 47387ea..5167a67 100644
--- a/gddo-server/background.go
+++ b/gddo-server/background.go
@@ -11,10 +11,16 @@
 	"log"
 	"time"
 
+	"cloud.google.com/go/trace"
+
 	"github.com/golang/gddo/gosrc"
 )
 
 func (s *server) doCrawl(ctx context.Context) error {
+	span := s.traceClient.NewSpan("Crawl")
+	defer span.Finish()
+	ctx = trace.NewContext(ctx, span)
+
 	// Look for new package to crawl.
 	importPath, hasSubdirs, err := s.db.PopNewCrawl()
 	if err != nil {
@@ -49,6 +55,10 @@
 }
 
 func (s *server) readGitHubUpdates(ctx context.Context) error {
+	span := s.traceClient.NewSpan("GitHubUpdates")
+	defer span.Finish()
+	ctx = trace.NewContext(ctx, span)
+
 	const key = "gitHubUpdates"
 	var last string
 	if err := s.db.GetGob(key, &last); err != nil {
diff --git a/gddo-server/client.go b/gddo-server/client.go
index 1455ead..af557a6 100644
--- a/gddo-server/client.go
+++ b/gddo-server/client.go
@@ -13,6 +13,7 @@
 	"net"
 	"net/http"
 
+	"cloud.google.com/go/trace"
 	"github.com/gregjones/httpcache"
 	"github.com/gregjones/httpcache/memcache"
 	"github.com/spf13/viper"
@@ -42,7 +43,7 @@
 	}
 	return &http.Client{
 		// Wrap the cached transport with GitHub authentication.
-		Transport: rt,
+		Transport: trace.Transport{Base: rt},
 		Timeout:   requestTimeout,
 	}
 }
diff --git a/gddo-server/config.go b/gddo-server/config.go
index 7c8e084..a1d15a8 100644
--- a/gddo-server/config.go
+++ b/gddo-server/config.go
@@ -50,6 +50,10 @@
 	ConfigDialTimeout     = "dial_timeout"
 	ConfigRequestTimeout  = "request_timeout"
 	ConfigMemcacheAddr    = "memcache_addr"
+
+	// Trace Config
+	ConfigTraceSamplerFraction = "trace_fraction"
+	ConfigTraceSamplerMaxQPS   = "trace_max_qps"
 )
 
 func loadConfig(ctx context.Context, args []string) (*viper.Viper, error) {
@@ -126,7 +130,6 @@
 
 	flags.StringP("config", "c", "", "path to motd config file")
 	flags.String(ConfigProject, "", "Google Cloud Platform project used for Google services")
-	// TODO(stephenmw): flags.Bool("enable-admin-pages", false, "When true, enables /admin pages")
 	flags.Float64(ConfigRobotThreshold, 100, "Request counter threshold for robots.")
 	flags.String(ConfigAssetsDir, filepath.Join(defaultBase("github.com/golang/gddo/gddo-server"), "assets"), "Base directory for templates and static files.")
 	flags.Duration(ConfigGetTimeout, 8*time.Second, "Time to wait for package update from the VCS.")
@@ -146,6 +149,8 @@
 	flags.Bool(ConfigDBLog, false, "Log database commands")
 	flags.String(ConfigMemcacheAddr, "", "Address in the format host:port gddo uses to point to the memcache backend.")
 	flags.String(ConfigGAERemoteAPI, "", "Remoteapi endpoint for App Engine Search. Defaults to serviceproxy-dot-${project}.appspot.com.")
+	flags.Float64(ConfigTraceSamplerFraction, 0.1, "Fraction of the requests sampled by the trace API.")
+	flags.Float64(ConfigTraceSamplerMaxQPS, 5, "Max number of requests sampled every second by the trace API.")
 
 	return flags
 }
diff --git a/gddo-server/main.go b/gddo-server/main.go
index e864409..0edaf8f 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -29,6 +29,7 @@
 	"time"
 
 	"cloud.google.com/go/logging"
+	"cloud.google.com/go/trace"
 	"github.com/spf13/viper"
 
 	"github.com/golang/gddo/database"
@@ -834,11 +835,12 @@
 }
 
 type server struct {
-	v          *viper.Viper
-	db         *database.Database
-	httpClient *http.Client
-	gceLogger  *GCELogger
-	templates  templateMap
+	v           *viper.Viper
+	db          *database.Database
+	httpClient  *http.Client
+	gceLogger   *GCELogger
+	templates   templateMap
+	traceClient *trace.Client
 
 	statusPNG http.Handler
 	statusSVG http.Handler
@@ -852,6 +854,18 @@
 		httpClient: newHTTPClient(v),
 	}
 
+	var err error
+	if proj := s.v.GetString(ConfigProject); proj != "" {
+		if s.traceClient, err = trace.NewClient(ctx, proj); err != nil {
+			return nil, err
+		}
+		sp, err := trace.NewLimitedSampler(s.v.GetFloat64(ConfigTraceSamplerFraction), s.v.GetFloat64(ConfigTraceSamplerMaxQPS))
+		if err != nil {
+			return nil, err
+		}
+		s.traceClient.SetSamplingPolicy(sp)
+	}
+
 	assets := v.GetString(ConfigAssetsDir)
 	staticServer := httputil.StaticServer{
 		Dir:    assets,
@@ -919,17 +933,21 @@
 	mux.Handle("/BingSiteAuth.xml", staticServer.FileHandler("BingSiteAuth.xml"))
 	mux.Handle("/C", http.RedirectHandler("http://golang.org/doc/articles/c_go_cgo.html", http.StatusMovedPermanently))
 	mux.Handle("/code.jquery.com/", http.NotFoundHandler())
-	mux.Handle("/_ah/health", http.HandlerFunc(serveHealthCheck))
-	mux.Handle("/_ah/", http.NotFoundHandler())
 	mux.Handle("/", handler(s.serveHome))
 
+	ahMux := http.NewServeMux()
+	ahMux.HandleFunc("/_ah/health", serveHealthCheck)
+
+	mainMux := http.NewServeMux()
+	mainMux.Handle("/_ah/", ahMux)
+	mainMux.Handle("/", s.traceClient.HTTPHandler(mux))
+
 	s.root = rootHandler{
-		{"api.", httpsRedirectHandler{apiMux}},
+		{"api.", httpsRedirectHandler{s.traceClient.HTTPHandler(apiMux)}},
 		{"talks.godoc.org", otherDomainHandler{"https", "go-talks.appspot.com"}},
-		{"", httpsRedirectHandler{mux}},
+		{"", httpsRedirectHandler{mainMux}},
 	}
 
-	var err error
 	cacheBusters := &httputil.CacheBusters{Handler: mux}
 	s.templates, err = parseTemplates(assets, cacheBusters, v)
 	if err != nil {
@@ -971,6 +989,10 @@
 	doc.SetDefaultGOOS(v.GetString(ConfigDefaultGOOS))
 
 	s, err := newServer(ctx, v)
+	if err != nil {
+		log.Fatal("error creating server:", err)
+	}
+
 	go func() {
 		for range time.Tick(s.v.GetDuration(ConfigCrawlInterval)) {
 			if err := s.doCrawl(ctx); err != nil {
diff --git a/gosrc/client.go b/gosrc/client.go
index 2f4663f..e6a4bf9 100644
--- a/gosrc/client.go
+++ b/gosrc/client.go
@@ -38,6 +38,8 @@
 		return nil, err
 	}
 
+	// Take the trace ID to group all outbound requests together.
+	req = req.WithContext(ctx)
 	for k, vs := range c.header {
 		req.Header[k] = vs
 	}