all: instrument build, vet, and run time This change adds a latency distribution for the build&vet stage, the vet stage, and the run stage of a snippet handled by the compile handler. For golang/go#44822 Change-Id: Icedce87492afadd6041efb05e6f0ed3cd12a01ba Reviewed-on: https://go-review.googlesource.com/c/playground/+/302770 Trust: Alexander Rakoczy <alex@golang.org> Run-TryBot: Alexander Rakoczy <alex@golang.org> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/go.mod b/go.mod index 8973383..9aeef12 100644 --- a/go.mod +++ b/go.mod
@@ -15,6 +15,7 @@ golang.org/x/mod v0.2.0 golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c google.golang.org/api v0.20.0 + google.golang.org/appengine v1.6.5 // indirect google.golang.org/genproto v0.0.0-20200312145019-da6875a35672 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.7 // indirect
diff --git a/internal/metrics/service.go b/internal/metrics/service.go index 6004e8d..00573a5 100644 --- a/internal/metrics/service.go +++ b/internal/metrics/service.go
@@ -7,6 +7,7 @@ package metrics import ( + "context" "errors" "fmt" "net/http" @@ -17,6 +18,7 @@ "contrib.go.opencensus.io/exporter/prometheus" "contrib.go.opencensus.io/exporter/stackdriver" "go.opencensus.io/stats/view" + "google.golang.org/appengine" mrpb "google.golang.org/genproto/googleapis/api/monitoredres" ) @@ -138,6 +140,30 @@ }), nil } +// GAEResource returns a *MonitoredResource with fields populated and +// for StackDriver. +// +// The resource will be in StackDrvier's gae_instance type. +func GAEResource(ctx context.Context) (*MonitoredResource, error) { + if !appengine.IsAppEngine() { + return nil, fmt.Errorf("not running on appengine") + } + projID, err := metadata.ProjectID() + if err != nil { + return nil, err + } + return (*MonitoredResource)(&mrpb.MonitoredResource{ + Type: "gae_instance", + Labels: map[string]string{ + "project_id": projID, + "module_id": appengine.ModuleName(ctx), + "version_id": appengine.VersionID(ctx), + "instance_id": appengine.InstanceID(), + "location": appengine.Datacenter(ctx), + }, + }), nil +} + // instanceGroupName fetches the instanceGroupName from the instance // metadata. //
diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..895efc1 --- /dev/null +++ b/metrics.go
@@ -0,0 +1,72 @@ +// Copyright 2021 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 main + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var ( + BuildLatencyDistribution = view.Distribution(1, 5, 10, 15, 20, 25, 50, 75, 100, 125, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 7000, 8000, 9000, 10000, 20000, 30000) + kGoBuildSuccess = tag.MustNewKey("go-playground/frontend/go_build_success") + kGoRunSuccess = tag.MustNewKey("go-playground/frontend/go_run_success") + kGoVetSuccess = tag.MustNewKey("go-playground/frontend/go_vet_success") + mGoBuildLatency = stats.Float64("go-playground/frontend/go_build_latency", "", stats.UnitMilliseconds) + mGoRunLatency = stats.Float64("go-playground/frontend/go_run_latency", "", stats.UnitMilliseconds) + mGoVetLatency = stats.Float64("go-playground/frontend/go_vet_latency", "", stats.UnitMilliseconds) + + goBuildCount = &view.View{ + Name: "go-playground/frontend/go_build_count", + Description: "Number of snippets built", + Measure: mGoBuildLatency, + TagKeys: []tag.Key{kGoBuildSuccess}, + Aggregation: view.Count(), + } + goBuildLatency = &view.View{ + Name: "go-playground/frontend/go_build_latency", + Description: "Latency distribution of building snippets", + Measure: mGoBuildLatency, + Aggregation: BuildLatencyDistribution, + } + goRunCount = &view.View{ + Name: "go-playground/frontend/go_run_count", + Description: "Number of snippets run", + Measure: mGoRunLatency, + TagKeys: []tag.Key{kGoRunSuccess}, + Aggregation: view.Count(), + } + goRunLatency = &view.View{ + Name: "go-playground/frontend/go_run_latency", + Description: "Latency distribution of running snippets", + Measure: mGoRunLatency, + Aggregation: BuildLatencyDistribution, + } + goVetCount = &view.View{ + Name: "go-playground/frontend/go_vet_count", + Description: "Number of vet runs", + Measure: mGoVetLatency, + TagKeys: []tag.Key{kGoVetSuccess}, + Aggregation: view.Count(), + } + goVetLatency = &view.View{ + Name: "go-playground/sandbox/go_vet_latency", + Description: "Latency distribution of vet runs", + Measure: mGoVetLatency, + Aggregation: BuildLatencyDistribution, + } +) + +// views should contain all measurements. All *view.View added to this +// slice will be registered and exported to the metric service. +var views = []*view.View{ + goBuildCount, + goBuildLatency, + goRunCount, + goRunLatency, + goVetCount, + goVetLatency, +}
diff --git a/sandbox.go b/sandbox.go index cc47954..83b5017 100644 --- a/sandbox.go +++ b/sandbox.go
@@ -36,6 +36,8 @@ "cloud.google.com/go/compute/metadata" "github.com/bradfitz/gomemcache/memcache" + "go.opencensus.io/stats" + "go.opencensus.io/tag" "golang.org/x/playground/internal" "golang.org/x/playground/internal/gcpdial" "golang.org/x/playground/sandbox/sandboxtypes" @@ -415,13 +417,25 @@ // sandboxBuild builds a Go program and returns a build result that includes the build context. // // An error is returned if a non-user-correctable error has occurred. -func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (*buildResult, error) { +func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (br *buildResult, err error) { + start := time.Now() + defer func() { + status := "success" + if err != nil { + status = "error" + } + // Ignore error. The only error can be invalid tag key or value + // length, which we know are safe. + stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)}, + mGoBuildLatency.M(float64(time.Since(start))/float64(time.Millisecond))) + }() + files, err := splitFiles(in) if err != nil { return &buildResult{errorMessage: err.Error()}, nil } - br := new(buildResult) + br = new(buildResult) defer br.cleanup() var buildPkgArg = "." if files.Num() == 1 && len(files.Data(progName)) > 0 { @@ -515,7 +529,7 @@ } if vet { // TODO: do this concurrently with the execution to reduce latency. - br.vetOut, err = vetCheckInDir(tmpDir, br.goPath) + br.vetOut, err = vetCheckInDir(ctx, tmpDir, br.goPath) if err != nil { return nil, fmt.Errorf("running vet: %v", err) } @@ -524,8 +538,18 @@ } // sandboxRun runs a Go binary in a sandbox environment. -func sandboxRun(ctx context.Context, exePath string, testParam string) (sandboxtypes.Response, error) { - var execRes sandboxtypes.Response +func sandboxRun(ctx context.Context, exePath string, testParam string) (execRes sandboxtypes.Response, err error) { + start := time.Now() + defer func() { + status := "success" + if err != nil { + status = "error" + } + // Ignore error. The only error can be invalid tag key or value + // length, which we know are safe. + stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoBuildSuccess, status)}, + mGoRunLatency.M(float64(time.Since(start))/float64(time.Millisecond))) + }() exeBytes, err := ioutil.ReadFile(exePath) if err != nil { return execRes, err
diff --git a/server.go b/server.go index 8352c51..8b82d23 100644 --- a/server.go +++ b/server.go
@@ -5,12 +5,15 @@ package main import ( + "context" "fmt" "net/http" "os" "strings" "time" + "cloud.google.com/go/compute/metadata" + "golang.org/x/playground/internal/metrics" "golang.org/x/tools/godoc/static" ) @@ -43,6 +46,17 @@ s.modtime = fi.ModTime() } } + gr, err := metrics.GAEResource(context.Background()) + if err != nil { + s.log.Printf("metrics.GaeService() = _, %q", err) + } + ms, err := metrics.NewService(gr, views) + if err != nil { + s.log.Printf("Failed to initialize metrics: metrics.NewService() = _, %q. (not on GCP?)", err) + } + if ms != nil && !metadata.OnGCE() { + s.mux.Handle("/statusz", ms) + } s.init() return s, nil }
diff --git a/vet.go b/vet.go index 905c03a..45c9175 100644 --- a/vet.go +++ b/vet.go
@@ -12,6 +12,10 @@ "os/exec" "path/filepath" "strings" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" ) // vetCheck runs the "vet" tool on the source code in req.Body. @@ -33,7 +37,7 @@ if err := ioutil.WriteFile(in, []byte(req.Body), 0400); err != nil { return nil, fmt.Errorf("error creating temp file %q: %v", in, err) } - vetOutput, err := vetCheckInDir(tmpDir, os.Getenv("GOPATH")) + vetOutput, err := vetCheckInDir(ctx, tmpDir, os.Getenv("GOPATH")) if err != nil { // This is about errors running vet, not vet returning output. return nil, err @@ -46,7 +50,19 @@ // go vet was able to run, not whether vet reported problem. The // returned value is ("", nil) if vet successfully found nothing, // and (non-empty, nil) if vet ran and found issues. -func vetCheckInDir(dir, goPath string) (output string, execErr error) { +func vetCheckInDir(ctx context.Context, dir, goPath string) (output string, execErr error) { + start := time.Now() + defer func() { + status := "success" + if execErr != nil { + status = "error" + } + // Ignore error. The only error can be invalid tag key or value + // length, which we know are safe. + stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kGoVetSuccess, status)}, + mGoVetLatency.M(float64(time.Since(start))/float64(time.Millisecond))) + }() + cmd := exec.Command("go", "vet") cmd.Dir = dir // Linux go binary is not built with CGO_ENABLED=0.