internal/metrics: add new package for recording metrics
Move service code from sandbox/metrics.go to
internal/metrics/service.go. This will enable sharing of the code for
the playground front-end, allowing us to record metrics there as well.
For golang/go#44822
Change-Id: I592486cdffd62dd6b9cee6cadb56ddc027788f59
Reviewed-on: https://go-review.googlesource.com/c/playground/+/302769
Trust: Alexander Rakoczy <alex@golang.org>
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/internal/metrics/service.go b/internal/metrics/service.go
new file mode 100644
index 0000000..6004e8d
--- /dev/null
+++ b/internal/metrics/service.go
@@ -0,0 +1,161 @@
+// 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 metrics provides a service for reporting metrics to
+// Stackdriver, or locally during development.
+package metrics
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "path"
+ "time"
+
+ "cloud.google.com/go/compute/metadata"
+ "contrib.go.opencensus.io/exporter/prometheus"
+ "contrib.go.opencensus.io/exporter/stackdriver"
+ "go.opencensus.io/stats/view"
+ mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
+)
+
+// NewService initializes a *Service.
+//
+// The Service returned is configured to send metric data to
+// StackDriver. When not running on GCE, it will host metrics through
+// a prometheus HTTP handler.
+//
+// views will be passed to view.Register for export to the metric
+// service.
+func NewService(resource *MonitoredResource, views []*view.View) (*Service, error) {
+ err := view.Register(views...)
+ if err != nil {
+ return nil, err
+ }
+
+ if !metadata.OnGCE() {
+ view.SetReportingPeriod(5 * time.Second)
+ pe, err := prometheus.NewExporter(prometheus.Options{})
+ if err != nil {
+ return nil, fmt.Errorf("prometheus.NewExporter: %w", err)
+ }
+ view.RegisterExporter(pe)
+ return &Service{pExporter: pe}, nil
+ }
+
+ projID, err := metadata.ProjectID()
+ if err != nil {
+ return nil, err
+ }
+ if resource == nil {
+ return nil, errors.New("resource is required, got nil")
+ }
+ sde, err := stackdriver.NewExporter(stackdriver.Options{
+ ProjectID: projID,
+ MonitoredResource: resource,
+ ReportingInterval: time.Minute, // Minimum interval for Stackdriver is 1 minute.
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Minimum interval for Stackdriver is 1 minute.
+ view.SetReportingPeriod(time.Minute)
+ // Start the metrics exporter.
+ if err := sde.StartMetricsExporter(); err != nil {
+ return nil, err
+ }
+
+ return &Service{sdExporter: sde}, nil
+}
+
+// Service controls metric exporters.
+type Service struct {
+ sdExporter *stackdriver.Exporter
+ pExporter *prometheus.Exporter
+}
+
+func (m *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if m.pExporter != nil {
+ m.pExporter.ServeHTTP(w, r)
+ return
+ }
+ http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+}
+
+// Stop flushes metrics and stops exporting. Stop should be called
+// before exiting.
+func (m *Service) Stop() {
+ if sde := m.sdExporter; sde != nil {
+ // Flush any unsent data before exiting.
+ sde.Flush()
+
+ sde.StopMetricsExporter()
+ }
+}
+
+// MonitoredResource wraps a *mrpb.MonitoredResource to implement the
+// monitoredresource.MonitoredResource interface.
+type MonitoredResource mrpb.MonitoredResource
+
+func (r *MonitoredResource) MonitoredResource() (resType string, labels map[string]string) {
+ return r.Type, r.Labels
+}
+
+// GCEResource populates a MonitoredResource with GCE Metadata.
+//
+// The returned MonitoredResource will have the type set to "generic_task".
+func GCEResource(jobName string) (*MonitoredResource, error) {
+ projID, err := metadata.ProjectID()
+ if err != nil {
+ return nil, err
+ }
+ zone, err := metadata.Zone()
+ if err != nil {
+ return nil, err
+ }
+ inst, err := metadata.InstanceName()
+ if err != nil {
+ return nil, err
+ }
+ group, err := instanceGroupName()
+ if err != nil {
+ return nil, err
+ } else if group == "" {
+ group = projID
+ }
+
+ return (*MonitoredResource)(&mrpb.MonitoredResource{
+ Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task
+ Labels: map[string]string{
+ "project_id": projID,
+ "location": zone,
+ "namespace": group,
+ "job": jobName,
+ "task_id": inst,
+ },
+ }), nil
+}
+
+// instanceGroupName fetches the instanceGroupName from the instance
+// metadata.
+//
+// The instance group manager applies a custom "created-by" attribute
+// to the instance, which is not part of the metadata package API, and
+// must be queried separately.
+//
+// An empty string will be returned if a metadata.NotDefinedError is
+// returned when fetching metadata. An error will be returned if other
+// errors occur when fetching metadata.
+func instanceGroupName() (string, error) {
+ ig, err := metadata.InstanceAttributeValue("created-by")
+ if errors.As(err, new(metadata.NotDefinedError)) {
+ return "", nil
+ } else if err != nil {
+ return "", err
+ }
+ // "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
+ ig = path.Base(ig)
+ return ig, nil
+}
diff --git a/sandbox/metrics.go b/sandbox/metrics.go
index 075be96..7fb213e 100644
--- a/sandbox/metrics.go
+++ b/sandbox/metrics.go
@@ -5,20 +5,10 @@
package main
import (
- "errors"
- "fmt"
- "net/http"
- "path"
- "time"
-
- "cloud.google.com/go/compute/metadata"
- "contrib.go.opencensus.io/exporter/prometheus"
- "contrib.go.opencensus.io/exporter/stackdriver"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
- mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
)
var (
@@ -112,148 +102,18 @@
}
)
-// newMetricService initializes a *metricService.
-//
-// The metricService returned is configured to send metric data to StackDriver.
-// When the sandbox is not running on GCE, it will host metrics through a prometheus HTTP handler.
-func newMetricService() (*metricService, error) {
- err := view.Register(
- containerCount,
- unwantedContainerCount,
- maxContainerCount,
- containerCreateCount,
- containerCreationLatency,
- ServerRequestCountView,
- ServerRequestBytesView,
- ServerResponseBytesView,
- ServerLatencyView,
- ServerRequestCountByMethod,
- ServerResponseCountByStatusCode)
- if err != nil {
- return nil, err
- }
-
- if !metadata.OnGCE() {
- view.SetReportingPeriod(5 * time.Second)
- pe, err := prometheus.NewExporter(prometheus.Options{})
- if err != nil {
- return nil, fmt.Errorf("newMetricsService(): prometheus.NewExporter: %w", err)
- }
- view.RegisterExporter(pe)
- return &metricService{pExporter: pe}, nil
- }
-
- projID, err := metadata.ProjectID()
- if err != nil {
- return nil, err
- }
- gr, err := gceResource("go-playground-sandbox")
- if err != nil {
- return nil, err
- }
-
- sd, err := stackdriver.NewExporter(stackdriver.Options{
- ProjectID: projID,
- MonitoredResource: gr,
- ReportingInterval: time.Minute, // Minimum interval for stackdriver is 1 minute.
- })
- if err != nil {
- return nil, err
- }
-
- // Minimum interval for stackdriver is 1 minute.
- view.SetReportingPeriod(time.Minute)
- // Start the metrics exporter.
- if err := sd.StartMetricsExporter(); err != nil {
- return nil, err
- }
-
- return &metricService{sdExporter: sd}, nil
-}
-
-// metricService controls metric exporters.
-type metricService struct {
- sdExporter *stackdriver.Exporter
- pExporter *prometheus.Exporter
-}
-
-func (m *metricService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if m.pExporter != nil {
- m.pExporter.ServeHTTP(w, r)
- return
- }
- http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
-}
-
-// Stop flushes metrics and stops exporting. Stop should be called before exiting.
-func (m *metricService) Stop() {
- if sde := m.sdExporter; sde != nil {
- // Flush any unsent data before exiting.
- sde.Flush()
-
- sde.StopMetricsExporter()
- }
-}
-
-// monitoredResource wraps a *mrpb.MonitoredResource to implement the
-// monitoredresource.MonitoredResource interface.
-type monitoredResource mrpb.MonitoredResource
-
-func (r *monitoredResource) MonitoredResource() (resType string, labels map[string]string) {
- return r.Type, r.Labels
-}
-
-// gceResource populates a monitoredResource with GCE Metadata.
-//
-// The returned monitoredResource will have the type set to "generic_task".
-func gceResource(jobName string) (*monitoredResource, error) {
- projID, err := metadata.ProjectID()
- if err != nil {
- return nil, err
- }
- zone, err := metadata.Zone()
- if err != nil {
- return nil, err
- }
- iname, err := metadata.InstanceName()
- if err != nil {
- return nil, err
- }
- igName, err := instanceGroupName()
- if err != nil {
- return nil, err
- } else if igName == "" {
- igName = projID
- }
-
- return (*monitoredResource)(&mrpb.MonitoredResource{
- Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task
- Labels: map[string]string{
- "project_id": projID,
- "location": zone,
- "namespace": igName,
- "job": jobName,
- "task_id": iname,
- },
- }), nil
-}
-
-// instanceGroupName fetches the instanceGroupName from the instance metadata.
-//
-// The instance group manager applies a custom "created-by" attribute to the instance, which is not part of the
-// metadata package API, and must be queried separately.
-//
-// An empty string will be returned if a metadata.NotDefinedError is returned when fetching metadata.
-// An error will be returned if other errors occur when fetching metadata.
-func instanceGroupName() (string, error) {
- ig, err := metadata.InstanceAttributeValue("created-by")
- if nde := metadata.NotDefinedError(""); err != nil && !errors.As(err, &nde) {
- return "", err
- }
- if ig == "" {
- return "", nil
- }
- // "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
- ig = path.Base(ig)
- return ig, err
+// 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{
+ containerCount,
+ unwantedContainerCount,
+ maxContainerCount,
+ containerCreateCount,
+ containerCreationLatency,
+ ServerRequestCountView,
+ ServerRequestBytesView,
+ ServerResponseBytesView,
+ ServerLatencyView,
+ ServerRequestCountByMethod,
+ ServerResponseCountByStatusCode,
}
diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go
index ffeb829..f65e642 100644
--- a/sandbox/sandbox.go
+++ b/sandbox/sandbox.go
@@ -31,11 +31,13 @@
"syscall"
"time"
+ "cloud.google.com/go/compute/metadata"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/stats"
"go.opencensus.io/tag"
"go.opencensus.io/trace"
"golang.org/x/playground/internal"
+ "golang.org/x/playground/internal/metrics"
"golang.org/x/playground/sandbox/sandboxtypes"
)
@@ -123,8 +125,12 @@
mux := http.NewServeMux()
- if ms, err := newMetricService(); err != nil {
- log.Printf("Failed to initialize metrics: newMetricService() = _, %v, wanted no error", err)
+ gr, err := metrics.GCEResource("go-playground-sandbox")
+ if err != nil && metadata.OnGCE() {
+ log.Printf("metrics.GceService(%q) = _, %v, wanted no error.", "go-playground-sandbox", err)
+ }
+ if ms, err := metrics.NewService(gr, views); err != nil {
+ log.Printf("Failed to initialize metrics: metrics.NewService() = _, %v, wanted no error", err)
} else {
mux.Handle("/statusz", ochttp.WithRouteTag(ms, "/statusz"))
defer ms.Stop()